C.W.K.
Stream
Lesson 02 of 05 · published

Why macOS Keeps Forgetting You

~12 min · codesign, tcc, gotcha, designated-requirement

Level 0Unlit
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Every rebuild, macOS looked at the app and saw a stranger. Because technically, it was one."

The Symptom

You grant Accessibility. You grant Microphone. You rebuild the app to fix a typo — and macOS asks for both permissions again. And again, every single build. It feels like a broken OS or a broken app. It's neither: it's a completely logical consequence of how macOS decides whether two binaries are the "same app."

The Cause: Designated Requirement

macOS keys TCC grants (Accessibility, Microphone, and friends) to an app's designated requirement — the code-signing rule that says "this is that app." A plain swift build produces an ad-hoc signature, whose designated requirement is pinned to the binary's cdhash. Change one character of source, and the cdhash changes, so the DR changes, so the app you just built does not match the app the user granted. macOS isn't forgetting — it's correctly concluding this is a different app that happens to share a name.

swift build -> ad-hoc signature -> DR pinned to cdhash
     |
     v  edit one line, rebuild
cdhash changes -> DR changes -> TCC: "who are you?" -> re-prompt forever

Fix: sign with ONE stable identity -> DR is stable -> grants persist

The Fix, and the Trap Inside the Fix

The fix is a single, stable, self-signed identity shared by all your apps — not a per-app certificate, which just multiplies the problem. Import it once per machine, have the build script sign with it, and the DR stops changing. Then there's a trap inside the fix worth knowing: security find-identity -v filters to Gatekeeper-valid identities, and a self-signed cert isn't (it reports as not trusted), so your identity appears to not exist. Detect it with security find-identity -p codesigning — no -v. One flag stands between "working as designed" and an hour of confusion. Finally, after the first properly-signed build, clear the stale ad-hoc-pinned grants once with tccutil reset and re-grant; from then on they persist.

When the OS keeps 'forgetting,' ask what identity it's actually keyed to. Permission systems don't track apps by name or path — they track a cryptographic identity. If grants evaporate, you haven't hit a bug; you've changed identity without meaning to. Find what the OS considers your identity, make it stable, and the symptom disappears at the root instead of being worked around.
Don't make a per-app certificate. The instinct on discovering this is 'fine, I'll sign each app with its own cert.' That's more certificates, more imports on every machine, more things to expire — solving a shared problem N times. One shared identity across the whole family of apps is strictly less work and strictly fewer failure modes.

Code

The detection trap: -v hides your self-signed identity·bash
# WRONG — filters to Gatekeeper-VALID identities.
# A self-signed cert reports CSSMERR_TP_NOT_TRUSTED and is HIDDEN,
# so this prints nothing and you conclude the identity isn't installed.
security find-identity -v

# RIGHT — lists codesigning identities without the validity filter.
security find-identity -p codesigning

# Sign with the ONE shared identity (build script does this automatically):
codesign --force --sign "cwk-codesign" --options runtime dist/YourApp.app

# After the FIRST properly-signed build, clear the stale ad-hoc grants once:
tccutil reset Accessibility com.example.yourapp
tccutil reset Microphone    com.example.yourapp
# Re-grant once. From here they persist across rebuilds.

External links

Exercise

Think of a time an OS or a service kept 'forgetting' something you'd already configured — permissions, a trusted device, a saved credential. What identity was it keying on, and what were you unknowingly changing between attempts? How would you make that identity stable?
Hint
The pattern generalizes far past code signing: whenever state keeps evaporating, something you believe is constant is actually varying — a hash, a device fingerprint, an install id, a container id. The fix is never 'grant it again more carefully'; it's finding the varying key and pinning it. Symptom-level fixes for identity churn always come back.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.