Skip to content
C.W.K.
Lesson 04 of 06 · published

The Grant Follows the Hash

~14 min · tcc, accessibility, code-signing, designated-requirement, war-story

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The ad-hoc requirement contains the code hash: a native code change and rebuild may invalidate its previous grant even at the same path." — Tinker README

Who Gets the Grant

To hide fixture apps for real, the lab needs a process with the Accessibility permission. The tempting shortcut is to grant it to whatever runs the tests — the shared Python runner, say. Tinker's README forbids exactly that: a grant to a general-purpose interpreter is a grant to every script it will ever run. Instead the lab builds a dedicated GUI app, Tinker Lab Check, with its own bundle ID and its own AppKit event loop, and only that app is granted. Dad adds it himself in the lab Mac's System Settings, under Privacy & Security, Accessibility. Nobody edits the privacy database, and a permission_needed receipt is never counted as a pass.

What macOS Remembers About an App

macOS's privacy system (TCC) does not remember a grant by file path. It remembers the app's designated requirement — a small expression the code signature carries that says what any future version must satisfy to count as the same app. For an app signed with a real certificate, the requirement names the bundle ID and the certificate, so a rebuilt and re-signed app keeps its grant. Tinker Lab Check is signed ad hoc: there is no certificate to name, so its requirement pins the code's hash itself. Rebuild it after a source change and, to TCC, it is a different app — at the same path, with the same name, showing as still enabled in Settings.

The Recipe That Grew Out of It

So the lab script has two modes. --build builds the helper and its fixture, writes its Info.plist, signs it ad hoc and runs it. --run-built verifies the existing bundle's signature, prints its designated requirement and the SHA-256 of its executable for the record, and runs it without rebuilding or re-signing a single byte. The workflow is: build once, have Dad grant that exact bundle, then always --run-built until the native code really changes — and when it does, rebuild deliberately and expect to ask for the grant again.

The Toggle That Didn't Take

One episode went further. After a rebuild, Dad toggled the helper on in Settings, and AXIsProcessTrusted() still returned false. The system log showed TCC recording an "Allowed" update for the new hash while access checks kept consulting the old one. What fixed it was not resetting anything: Dad removed only Tinker Lab Check from the Accessibility list and added the same current bundle again, and the unchanged helper then passed. The README records the lesson precisely — do not reset other apps' grants, and do not rebuild between that registration and the retry, or you are back where you started.

Code

Run the already-granted bundle without touching it (from scripts/lab-visibility.sh)·bash
APP="$PWD/.build/Tinker Lab Check.app"
# After a manual grant, preserve the exact ad-hoc identity Dad approved.
# Native source changes require --build and may need a new console grant.
codesign --verify --deep --strict "$APP"
codesign -dr - "$APP" 2>&1                        # print the designated requirement
shasum -a 256 "$APP/Contents/MacOS/TinkerLabCheck"  # and the executable digest
open -n -W "$APP" --args --result "$RESULT" --parent "$$"
Seeing the difference yourself on any two builds·bash
# Ad-hoc sign a scratch app twice with a code change in between, and compare.
codesign --force --sign - Scratch.app
codesign -dr - Scratch.app        # designated => cdhash H"…"  (pins this code)

# A certificate-signed app instead shows something like:
# designated => identifier "com.example.app" and certificate leaf = H"…"

External links

Exercise

Your test helper needs a privacy permission. Write the short runbook a teammate should follow: how the helper is built and signed, who grants it and where, how to run it again without invalidating the grant, what to do after a native code change, and the one thing they must never do if the grant seems not to take.
Hint
Build and ad-hoc sign once; a person grants that exact bundle in System Settings; re-run the built bundle without rebuilding or re-signing; after a code change, rebuild on purpose and expect a new grant. If a grant does not take, read the log, then remove and re-add only that app — never reset other apps' permissions, never edit the TCC database, and never rebuild between the re-registration and the retry.

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 in — Please sign in to comment.

No comments yet — be the first.