Module 9
Permissions, security, and distribution
Apple's security model is the part of macOS development with the least intuition transfer from web or mobile work, and the part most likely to make you think your code is broken when it isn't.
TCC — the consent database
Transparency, Consent, and Control is the system that gates access to protected resources. Each capability is a separate grant, stored per-app, and the user can revoke any of them at any time.
| Capability | How it's requested | Timing |
|---|---|---|
| Microphone | AVCaptureDevice.requestAccess(for: .audio) | Prompts on first use; checked live |
| Speech Recognition | Declared in Info.plist | Prompts on first use |
| Accessibility | AXIsProcessTrustedWithOptions | Read at launch only |
| Input Monitoring | Implicit with event taps | Prompts on first use |
Any Info.plist key of the form NS…UsageDescription is a string shown to the user in the permission dialog. If you request a capability without declaring its usage string, the app crashes immediately — no warning, no recoverable error. It's a deliberate design choice to make undeclared access impossible.
Trap · Accessibility needs a relaunch in practice
Treat the Accessibility trusted state as unreliable after a live toggle. AXIsProcessTrusted() is known to return stale values when permission is granted while the app is running, and Apple's own developer-support guidance is to quit and relaunch for the change to take effect reliably. Sometimes it does flip live; you cannot depend on it.
Symptomatically this is brutal, because nothing errors — the hotkey just stays dead and the paste silently fails. Assume a relaunch is required, and ship an explicit "Restart Spoke" button next to the permission prompt rather than making users work it out.
The sandbox and entitlements
The App Sandbox restricts an app to its own container: no arbitrary filesystem access, no controlling other apps, no unrequested network. Entitlements are signed key-value pairs that punch specific holes in it.
Spoke turns the sandbox off, and it's worth being clear about why rather than treating it as a shortcut. A dictation app's entire function is to send synthetic keystrokes into other applications. That is precisely what the sandbox exists to prevent, and there is no entitlement for it. Every app in this category — Wispr Flow included — ships unsandboxed.
One distinction that a knowledgeable reader will hold you to: listening is allowed, posting is not. A sandboxed app granted Accessibility or Input Monitoring can install a CGEventTap and observe keystrokes. What it cannot do is post synthetic events with CGEventPost, or drive another app through AXUIElementCreateApplication. Spoke needs the posting half, so the sandbox has to go.
The consequence: no Mac App Store. App Store distribution requires sandboxing. You distribute directly, which is entirely normal for Mac utilities and has the nice side effect of no 30% cut.
Code signing and notarization
Three distinct things that are easy to conflate:
Code signing attaches a cryptographic signature identifying you as the developer and proving the binary hasn't been modified. Xcode does this automatically during development with a local certificate.
The hardened runtime is a set of runtime protections — no code injection, no unsigned libraries, no debugger attachment. Required for notarization.
Notarization is Apple scanning your signed app for malware and issuing a ticket. Not review — it's automated, usually finishing in minutes. Stapling attaches that ticket to the app so it validates offline.
Without notarization, Gatekeeper tells users your app "cannot be opened because Apple cannot check it for malicious software," with no obvious way forward. Most people quit there. Notarization is the difference between a project and a product.
The distribution pipeline, once you're ready:
- Join the Apple Developer Program (about $99/year — the only unavoidable cost of shipping this).
- Create a Developer ID Application certificate.
- Archive in Xcode: Product → Archive.
- Export with Developer ID signing.
- Submit with
xcrun notarytool submit --wait. - Staple with
xcrun stapler staple Spoke.app. - Ship a
.dmgor.zip.
Budget an afternoon the first time. It's fiddly and well-documented, and every subsequent release is a single script.