Why macOS permissions vanish every time you rebuild
Updated 2026 · a 6-minute read · for developers
You're building a Mac app that needs Accessibility (or Screen Recording, or Input Monitoring). You grant it in System Settings, everything works, you make a one-line change, rebuild — and the app is silently denied again. The toggle still looks enabled. Toggling it off and on fixes it, until the next build.
This is one of the most disorienting parts of Mac development, because nothing reports an error. Your permission check just quietly returns false. Here's what's actually happening.
Advertisement
TCC identifies apps by signature, not by path
macOS stores privacy permissions in a database usually referred to as TCC (Transparency, Consent, and Control). When you flip a toggle in Privacy & Security, TCC records the grant against the app's identity.
Crucially, that identity is not "the app at this file path." It's the app's designated requirement — a rule derived from the code signature that describes what a binary must satisfy to count as this app. That design is deliberate and good: it means swapping a malicious binary into the same path doesn't inherit the original app's permissions.
The problem appears when your designated requirement isn't stable between builds.
Ad-hoc signing is the usual culprit
If you sign with codesign -s - (ad-hoc), there's no certificate involved. With no
signing identity to anchor to, the designated requirement ends up derived from the code
itself — effectively a hash of the binary.
Change one line of code, rebuild, and the hash changes. The new binary no longer satisfies the stored requirement, so TCC treats it as a different app and denies it. The old grant is still sitting in the database, matching a binary that no longer exists — which is exactly why the toggle still appears on while your app gets nothing.
You can see the requirement for yourself:
codesign -d -r- /Applications/YourApp.app
An ad-hoc build will show a requirement with a cdhash. That's the tell. A build
signed with a certificate shows something referencing the certificate instead.
The fix: a stable local signing identity
You don't need a paid Apple Developer account to solve this. A self-signed certificate is enough, because what matters is that the certificate stays the same across builds, not that anyone trusts it.
Create a self-signed code signing certificate in your login keychain (Keychain Access can do
this via Certificate Assistant, or you can script it with openssl and
security import), then sign with it:
codesign --force --deep --sign "Your Local Signing" /path/to/YourApp.app
Now check the requirement again. You should see something along these lines:
designated => identifier "com.you.yourapp" and certificate leaf = H"<hash>"
That requirement depends on your bundle identifier and your certificate — neither of which changes when you edit code. Rebuild as often as you like; the grant survives.
A few gotchas worth knowing in advance
- codesign accepts untrusted certificates. You do not need to mark it trusted in Keychain Access, and you shouldn't. Leaving it untrusted means nothing else on the system will accept code signed by it — the certificate does its job here purely as a stable anchor.
- OpenSSL 3 defaults can break the keychain import. Newer OpenSSL writes
PKCS#12 files with AES-256-CBC encryption that the macOS keychain won't import. Adding
-legacywhen exporting the.p12fixes it. An empty export password also tends to fail — use a real one. - Sign nested content first. Frameworks and helpers inside the bundle need to be signed before the outer app, or verification fails.
Advertisement
The other trap: duplicate copies of your app
Even with stable signing, there's a second failure mode that wastes hours, and it's worth checking before you go hunting for signature problems.
If two copies of your app exist — say a build output at build/YourApp.app and an
installed copy in /Applications — macOS may register both in the
Privacy list. They can look identical in the UI, showing the same name and icon. You grant one,
but the copy you're actually running is the other, so your permission check keeps returning
false while the toggle sits there enabled.
The signals to watch for:
- Two entries with the same app name in the Accessibility list.
- Resetting permissions reports success more than once for the same bundle ID:
If that prints a success line twice, you have two registered copies.tccutil reset Accessibility com.you.yourapp
The fix is to delete the stray copy, reset the permission for the bundle ID, then grant once to the single remaining app. It's worth making your build script remove its staging copy after installing, so the situation can't recur.
Debugging tip: your logs may not reach you
If your app is a background agent (LSUIElement), NSLog output doesn't
reliably reach the unified log, which makes "is the permission actually active?" surprisingly
hard to answer. A simple workaround is to write the result of your permission check into
UserDefaults at launch along with a timestamp, then read it from the terminal:
defaults read com.you.yourapp
That gives you ground truth about what the running process sees, rather than what the Settings UI claims. The timestamp matters — it tells you whether you're reading a fresh result or a stale one from a previous build.
Summary
- TCC binds permission grants to the code signature, not the file path.
- Ad-hoc signing produces a hash-based requirement that changes on every build, silently invalidating grants.
- A self-signed (and deliberately untrusted) certificate produces a stable requirement that survives rebuilds. No paid account needed.
- Check for duplicate registered copies before blaming the signature.
- For background agents, record permission state in
UserDefaultsrather than trusting logs or the Settings toggle.
All of the above came out of building Barkeep, a free menu bar organizer for macOS. It's open source, so the signing script and the permission handling are there to read if you want a working reference.
Related: Why menu bar apps ask for Accessibility · How to rearrange Mac menu bar icons