Skip to content

2. Launch only as a certificate-signed bundle through LaunchServices

Updated View as Markdown

Status: accepted · Date: 2026-08-29 · Supersedes: —

Context and Problem Statement

A process tap started from a terminal is created successfully, delivers buffers at exactly the right rate, and every sample is zero. No error, no prompt, no log line. TCC attributes the request to the responsible process — the terminal — which holds no audio permission and does not prompt for one.

A second, independent failure sits on top of it. codesign --sign - makes the app’s designated requirement its own cdhash, which changes on every rebuild. TCC stores that hash in the grant’s csreq, so a rebuild orphans the permission while the row stays in place still reading allowed. The two services then diverge — kTCCServiceMicrophone keeps working, kTCCServiceAudioCapture runs and returns zeros — which is what makes the pair so easy to conflate.

Considered Options

  • Run the binary directly, and accept configuring TCC for the terminal.
  • Ad-hoc signing (codesign --sign -) of the bundle, the zero-setup default.
  • A self-signed certificate identity created once by setup-signing.sh, with the bundle launched through LaunchServices.

Decision Outcome

Capture runs only from build/Ambient.app launched via open -a, signed with the certificate identity setup-signing.sh creates once.

Granting the terminal the permission does not help: the responsible-process attribution holds regardless of how the app is signed, measured after the signing fix as call peak 0.000 for a direct launch against 0.869 for open -a. The certificate makes the designated requirement identifier "uk.ambient.cli" and certificate leaf = …, which survives rebuilds, where the ad-hoc cdhash does not.

Consequences

Every capture path — tap, record, the menu bar app — inherits the bundle launch. Under LaunchServices the working directory is / and stderr goes nowhere, which is why the settings page is embedded with include_str! and the level meter is mirrored to the status file.

Because the failure is silent, tap and record diagnose it at runtime: they sample kAudioProcessPropertyIsRunningOutput during capture and, if the call track is flat while something was demonstrably playing, name the likely cause — launch method first (parent pid 1 means LaunchServices, anything else means a shell), then the cdhash. On a managed Mac a PPPC profile denying kTCCServiceAudioCapture produces identical symptoms, so the cdhash is ruled out before IT is blamed.

Confirmation

CI runs ./make-app.sh and then codesign -v --verbose=2 build/Ambient.app, plutil -lint on the Info.plist, and a test that the bundle’s executable exists — so an unsigned or malformed bundle fails the rust job. It cannot check that the grant survived: no runner can answer a TCC prompt. A person confirms that half by rebuilding and re-running the tap through open -a, checking the call peak is non-zero rather than 0.000; the truth tables in when it does not work are the evidence for which failure a given symptom is.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close