macOS: TCC permission prompts recur on every auto-update (interactive CLI never uses ClaudeCode.app)
macOS: TCC permission prompts recur on every auto-update (interactive CLI never uses ClaudeCode.app)
Env: macOS 26 (Darwin 25.5.0), Apple Silicon, Claude Code 2.1.246, native installer.
Symptom
"2.1.245" would like to access files on a network volume. reappears after every auto-update.
The dialog is titled with the version number because the subject has no bundle.
Unanswered prompts queue: on 2026-08-25 four were raised (16:13, 20:14, 20:44, 21:45) before any was answered.
Cause
tccd records consent for the CLI keyed by PATH, not by signing identity:
Publishing <TCCDEvent: type=Create, service=kTCCServiceSystemPolicyNetworkVolumes,
identifier_type=Path, identifier=~/.local/share/claude/versions/2.1.245>
BUNDLE_ATTRIBUTION: executable path ... resolves to attributed bundle: (null)
IDENTITY_ATTRIBUTION: ... type 1
(Ghostty in the same log is type: 0.) The binary is a bare Mach-O — codesign -dv reportsInfo.plist=not bound. Since the install path carries the version (versions/<semver>) and~/.local/bin/claude is a symlink that execve resolves, every update presents a brand-new TCC subject.
You already ship the fix, but only wire it to one entrypoint
ensureAppBundle() hard-links the running binary into~/.local/share/claude/ClaudeCode.app/Contents/MacOS/claude (CFBundleIdentifiercom.anthropic.claude-code) and re-execs through it with macDisclaimResponsibility: true.
It is called only from the --bg-pty-host entrypoint, and skipped when the argv after --
contains --bg-spare. The interactive CLI never re-execs through the bundle.
Observed consequence: on this machine ClaudeCode.app/Contents/MacOS/claude was 12 versions stale
(2.1.234, mtime Aug 17) while the running version was 2.1.246.
Second, deeper problem: link() does not give a stable identity
ensureAppBundle uses link(), so the bundle path and the versioned path are two names for one inode.
For a hard-linked inode the kernel returns whichever NAME is currently in the vnode name cache.
Isolated reproducer (any Mach-O, two hard links, one long-running process, sampling proc_pidpath
on the SAME pid while stat-ing the other name in between):
observed reported basenames: {'nameB': 8, 'nameA': 12}
So even processes that DO go through the bundle can be attributed to the versioned path.
A single-name inode would fix it, but a full copy or an APFS clone placed atClaudeCode.app/Contents/MacOS/claude is killed on exec (rc=137), while the same clone runs fine
outside the bundle — consistent with the bundle carrying no _CodeSignature/CodeResources
(codesign --verify --strict on the .app fails with code has no resources).
Suggested fixes
- Call the bundle relaunch from the interactive entrypoint too, not only
--bg-pty-host. - Ship
ClaudeCode.appas a properly sealed, signed bundle whose executable is its own file, so the
inode has exactly one name and TCC can key on the bundle identifier.
- Failing both, install to a stable path and keep the version out of the executable's pathname.
Note for Apple (separate)
proc_pidpath / any path-based TCC identity is ambiguous for a multiply-linked executable; consent
recorded as identifier_type=Path can therefore be attributed to a different name than the one used
at execve.