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.
3 Comments
Still reproducing on 2.1.260 (macOS 26.6.2, build 25G83, Apple Silicon, native installer). Confirming both mechanisms described here, plus the cumulative cost measured from the user TCC database.
Bundle exec is still killed.
Rebuilding the bundle from scratch with the current 2.1.260 binary and a hand-written
Info.plistgives the samerc=137, so this is not caused by the staleness. On this machineClaudeCode.app/Contents/MacOS/claudehas not been refreshed since Aug 30.Cumulative churn in
~/Library/Application Support/com.apple.TCC/TCC.db, 2026-05-08 through 2026-09-04:Today's update produced 7 new rows in 13 seconds: Desktop, Documents, Downloads, AppData, MediaLibrary, and two
kTCCServiceFileProviderDomainrows for iCloud Drive and OneDrive. One of them ended up recorded as denied, simply because a burst of seven dialogs is hard to read.One detail that may simplify the fix. The
csreqstored on those rows carries no version pin:codesign -v -R=<that requirement>passes for 2.1.258, 2.1.259 and 2.1.260 alike. So suggestion 3 in the report (stable install path, version kept out of the executable pathname) is sufficient on its own: the same path would retain its grants across updates without the bundle being involved at all. I have a local wrapper doing exactly that (fixed path, atomic replace gated on acodesign -Rcheck). It resolves and runs correctly (claude doctorreportsnative (2.1.260)at the fixed path, auto-updates still enabled). Whether the grants actually survive a version swap at that path is the one thing I have not confirmed end to end yet, and I will follow up after the next update.+1 for this. Aggravating given the release cadence - at least two new permissions to add, and two to retire, per version bump. Will someone at Anthropic _please_ seal the app properly or leave the version out of the executable path - this is just sloppy work.
Still reproducing on 2.1.289, macOS 27.0 (26A428), Apple Silicon, native installer. Two data points that I don't think are in the thread yet.
1. The hard-link ambiguity hits the
--bg-pty-hostpath in practice, seen directly in tccd attribution.A pty-host started via
ClaudeCode.app/Contents/MacOS/claude(bundle exec andversions/2.1.289share one inode, link count 2) produced this split over 6 hours ofAUTHREQ_ATTRIBUTIONfor the same pid:The path alternates within minutes. When it resolves to the versioned name, tccd logs
static code for: identifier ~/.local/share/claude/versions/2.1.289, type: 1, so the request is treated as a path client and a "2.1.289" dialog appears even though the process came up through the bundle. A pty-host from the previous day, which was linked to 2.1.288 before the bundle was relinked, is now attributed only toversions/2.1.288. Result in the system TCC.db:kTCCServiceSystemPolicyAllFilesrows for 10 distinctversions/<x>paths (client_type 1), plus a separate bundle row.The child REPL is spawned via the versioned path (
ps -o commshowsversions/2.1.289for the pty-host's child). That is one source of lookups of the other name. The vnode-name-cache explanation is still my hypothesis, but the effect at the tccd level is consistent with the reproducer in the issue body.2. On macOS 27.0, an APFS clone at the bundle path is not killed with 137. It blocks behind a Gatekeeper prompt.
cp -c -p versions/2.1.289 ClaudeCode.app/Contents/MacOS/claude(own inode, identical bytes) and thenClaudeCode.app/Contents/MacOS/claude --versionhangs in_dyld_start, while CoreServicesUIAgent logspresent code-evaluation promptand syspolicyd logsError checking with notarization daemon: 3. The hard-linked inode starts in ~1.6 s. Same root cause as #70647 (unsealed bundle). On 27.0, users who try the single-inode workaround locally get a GUI prompt rather than a crash.Also worth noting:
ensureAppBundlelinks the caller'sprocess.execPath. A bg session launched by an older, still-running session therefore relinks the bundle backwards to that older version.A properly sealed bundle whose executable is its own file (suggestion 2 in the issue body) would address both points.