"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.