[BUG] claude binary re-registers as new app in macOS Privacy & Security on every version update (versioned install path, not code signing)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Summary
Every Claude Code update forces re-authorization of macOS privacy permissions (Full Disk Access, Automation, etc.) because each version installs to a unique versioned path. macOS TCC resolves symlinks to the real executable path and treats each version as a distinct application.
Evidence
Full Disk Access in System Settings shows each version as a separate, unrecognized app:
<img width="500" height="333" alt="Image" src="https://github.com/user-attachments/assets/d5d9f8e5-cf10-443c-b049-e80e20f49ec6" />
By contrast, Claude Desktop appears as a single stable "Claude" entry and does not exhibit this behavior.
codesign output (2.1.111)
Executable=/Users/<user>/.local/share/claude/versions/2.1.111
Identifier=com.anthropic.claude-code
Authority=Developer ID Application: Anthropic PBC (Q6L2SF6YDW)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
TeamIdentifier=Q6L2SF6YDW
The signing identity is stable and correct — this is NOT a code signing issue.
Root Cause
macOS TCC tracks permissions by resolved executable path, not bundle identifier. Each update installs to ~/.local/share/claude/versions/<version>/, so every release is a new path = new app to TCC.
Impact
- Re-authorization required after every update
- Orphaned permission entries accumulate in System Settings
- Particularly disruptive for integrations requiring Full Disk Access (e.g., iMessage plugin)
Suggested Fix
Register a stable, non-versioned symlink path (e.g., ~/.local/share/claude/current) with TCC instead of the versioned path — and ensure the symlink is what macOS tracks. This is a standard pattern for versioned Electron/CLI app installs.
Environment
- macOS 26.3.1
- Claude Code 2.1.111 (affects all versions observed: 2.1.97+)
- Installed via native installer
What Should Happen?
New versions of Claude should not require re-authorizing full-disk access.
Error Messages/Logs
Executable=/Users/<user>/.local/share/claude/versions/2.1.111
Identifier=com.anthropic.claude-code
Authority=Developer ID Application: Anthropic PBC (Q6L2SF6YDW)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
TeamIdentifier=Q6L2SF6YDW
Steps to Reproduce
Steps to Reproduce
- Install Claude Code and grant Full Disk Access when prompted
- Open System Settings → Privacy & Security → Full Disk Access
- Confirm
claudeappears in the list with the toggle enabled - Install any Claude Code update (version increment, any size)
- Attempt to use a feature requiring Full Disk Access (e.g., iMessage plugin)
- Observe: macOS prompts for Full Disk Access again
- Return to System Settings → Privacy & Security → Full Disk Access
- Observe: the new version appears as a separate entry; the previous version entry remains, toggle off
Expected Result
Claude Code retains Full Disk Access permission across updates — single entry persists in System Settings.
Actual Result
Each version creates a new entry named after its version number (e.g., 2.1.110, 2.1.111). Previous entries remain as orphaned, disabled entries. Permissions must be re-granted after every update.
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.110
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
iTerm2
Additional Information
_No response_
16 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
this is a good idea. need this
I see other duplicates but they keep getting closed out instead of actually getting addressed.
spent a week fighting this exact thing shipping a macOS automation MCP. TCC keys rows by canonical executable path plus designated requirement, so versions/<n> spawns a fresh TCC.db row on every bump. the symlink workaround helps only if the launcher exec's through the symlink AND dyld doesn't resolve to the real path first (which it does under hardened runtime by default). what actually stuck for us: a stable .app bundle with a thin exec stub that spawns the versioned binary. TCC keys off the stub and survives updates. orphaned rows never GC so the Privacy pane grows forever unless you manually
tccutil resetper-service.Update based on community feedback and additional research
Building on the writeup @m13v did above — their hands-on experience shipping a macOS automation MCP was enormously helpful in identifying why the symlink approach won't hold. A few refinements and additions to tighten the technical detail and broaden the scope of impact.
Symlink approach will NOT work (confirming prior comment)
As noted above, a stable symlink won't survive macOS hardened runtime. More precisely: the kernel canonicalizes the executable path at
execve()time for code signing verification, so TCC evaluates the real versioned path regardless of how the binary was invoked. Implementing a symlink-based fix would appear to work in testing and fail in production.Correct fix: stable .app bundle with a launcher stub
TCC keys rows by canonical executable path + designated requirement. The working pattern:
.appbundle at a stable, non-versioned path (e.g.,/Applications/Claude Code.app/)Contents/MacOS/— this is the path TCC tracksWorth flagging: the current codesign output shows
Executable=/Users/<user>/.local/share/claude/versions/2.1.111— a raw Unix-style binary, not an.appbundle. Adopting this fix is a meaningful packaging change (proper bundle layout,Info.plist, etc.), not a drop-in tweak. Precedent exists though — Electron apps (VS Code, Slack, Discord) solved this exact problem with stable.appbundles + versioned resources inside, often using Sparkle for in-place updates. There's a well-worn path to follow.Scope of impact is broader than Full Disk Access
TCC uses the same path-based keying for every service it gates. Users are hit across:
Any integration that depends on any of these regresses on every update.
Orphaned rows never GC
As @m13v noted, macOS does not garbage-collect stale TCC rows. The Privacy & Security pane accumulates one dead entry per historical version indefinitely. Beyond the clutter, this actively confuses users — stale entries look like broken permissions, which drives avoidable support load.
User-side cleanup options:
Targeted (preferred, preserves other apps' permissions):
Hoping this gets addressed soon. Happy to test anything.
Confirming this bug.
Seeing this as well, disabled auto updates for claude code which mitigates the issue, but would be nice to be able to have it enabled.
Another here with the same bug.
same issue
Confirming the analysis from @m13v and @blouque. Adding one data point they didn't have: the background agent daemon path is a separate Mach-O invocation with the same problem.
When you run
claude agents, the process tree is:PID 5759 (
--bg-spare) is the process macOS attributes permission prompts to. It's a detached process with no terminal parent, so macOS can't roll permissions up to the terminal emulator — it evaluates the versioned binary path directly. Same TCC row-per-version problem, same orphaned entries on upgrade.This means the
.appbundle stub fix (@blouque's writeup) needs to cover both the CLI entry point and the background agent binary. A fix scoped only to~/.local/bin/claudewould still leave the--bg-spareworker presenting as a raw versioned path.Filed #61314 specifically to track the agent path. Linking here as the canonical issue.
+1, and adding a reproducible data point that confirms the fix direction, building on @m13v / @blouque / @jrignacio (whose root-cause analysis is correct: TCC keys unbundled-CLI rows by canonical executable path, so
versions/<n>spawns a freshTCC.dbrow every bump).**Confirmed: a stable-path launcher carries the grant across versions — but only if the process is launched as the launcher.**
Env: macOS 15 (Darwin 25.3), native install,
~/.local/bin/clauderepointed to~/.local/share/claude/versions/<n>on each auto-update.execv()s the current version binary.kTCCServiceSystemPolicyAllFiles | ~/.local/bin/claude-launcher | client_type=1— keyed to the stable path.A/B, both spawned by
launchd(fresh/detached, identical except the program), each reading a folder the version binary had no grant for:| Program launched | Result |
|---|---|
|
…/versions/2.1.159directly | prompted ("2.1.159 would like to access…"); grant keyed to the version path ||
…/claude-launcher(execv→ 2.1.159) | silent; covered by the launcher's FDA; no version-path row created |Same service, same context — the only variable is the launched program. So macOS pins the TCC identity to the program that was launched, and the later
execv()to the versioned binary does not reset it. A stable-path launcher fixes this cleanly.The catch (reinforcing @jrignacio): it only helps for processes launched as the launcher. Wrapping interactive shells isn't enough — the background-agent/daemon path (
claude agents, FleetView) spawns the versioned Mach-O directly, so those keep re-prompting. A user-side launcher would have to interpose on~/.local/bin/claudeitself and then fight the auto-updater that repoints it — fragile.Request: ship the stable launcher upstream — a fixed-path, signed launcher stub (or a proper
.appbundle) that the CLI and every internally-spawned agent/daemon exec through, so the entire process tree inherits one stable TCC identity. Grant once, survives every update. Happy to test a build.Confirming on 2.1.196 (macOS 26.x) and adding the naming half of this symptom:
Each version installs as a bare Mach-O at
~/.local/share/claude/versions/<version>with no app bundle and no embedded__TEXT,__info_plistsection. So TCC has noCFBundleNameto show and falls back to the file name — which is the literal version string. The prompt reads:Users get a permission prompt that doesn't even say "Claude," from a process they can't recognize, on every update. Verified locally:
otool -l versions/2.1.196→ no__info_plistsectionbasenameof the path =2.1.196Q6L2SF6YDW), so editing the binary to embed a bundle name would break itThe stable-
current-symlink fix helps re-registration, but TCC resolves symlinks to the real target, so the displayed name stays the versioned filename. Two clean upstream fixes:claude(keep the version in the dir:versions/<ver>/claude) — renaming doesn't alter bytes, so the Developer-ID signature stays valid, and the prompt shows "claude".Info.plist(CFBundleName=Claude) in the Mach-O__TEXT,__info_plistat sign time → prompt shows "Claude".Either makes the prompt recognizable as Claude instead of a bare number.
Correction on the orphan-row cleanup:
tccutil reset SystemPolicyAllFiles com.anthropic.claude-codedoes not actually clear the accumulated version rows. Verified on 2.1.196 (macOS 26 / Darwin 25):The "Successfully reset" message is misleading. These rows store the client as a path (
.../versions/<ver>,client_type=1), buttccutil reset <service> <bundle-id>matches by bundle identifier — so it resets a (nonexistent) bundle-keyed entry and silently no-ops on the path-keyed ones. There's no path-targeted form oftccutil reset. The only thing that clears them istccutil reset <service>with no identifier, which wipes every app's grant for that service — not worth it. (The rows are harmless anyway; most point at already-deleted version binaries.)This reinforces the core ask: a stable-path launcher /
.appstub so there's one TCC identity to grant — and clean up — instead of an un-targetable pile of path-keyed rows.Separately, FWIW a user-side compiled launcher stub (fixed path, ad-hoc + hardened-runtime signed,
execvs the currentversions/<n>binary,CFBundleName=Claudeembedded so the prompt reads "Claude") does carry an FDA grant across updates for the interactiveclaudepath — matching @ifrahdrordavid's A/B result. It does not cover theclaude agents/--bg-sparedaemon path (@jrignacio), which still needs the upstream fix.Reproducing on 2.1.202 (native, arm64). Adding one detail I don't see captured here yet: the in-app disable does not stop it. My
~/.claude.jsonalready has"autoUpdates": false,"autoUpdatesProtectedForNative": true,"installMethod": "native"— yet three new versioned binaries landed over three consecutive days (versions/2.1.200,2.1.201,2.1.202), so the protected native updater overridesautoUpdates:false, producing a fresh TCC identity and a fresh Full Disk Access prompt roughly daily.Mechanics confirmed on this version:
~/.local/bin/claude→~/.local/share/claude/versions/2.1.202— a bare Mach-O, signedIdentifier=com.anthropic.claude-code, TeamQ6L2SF6YDW, butInfo.plist=not bound.~/.local/share/claude/ClaudeCode.app(CFBundleIdentifier=com.anthropic.claude-code) — andContents/MacOS/claudeis the same inode as the versioned binary (the updater hardlinks it in). But the CLI shim points at the versioned path, not the bundle.``
`ClaudeCode.app/Contents/MacOS/claude (stable bundle path, parent)
└─ versions/2.1.202 (versioned path, child — touches files → TCC keys FDA here)
~/.local/bin/claudeSince TCC also resolves symlinks to the real path before checking, a stable
or aversions/current` symlink doesn't help either.+1 on the stable-path fix — having the worker child exec a fixed path (e.g. the bundle exec, or
versions/current) so a single FDA grant persists across updates.Adding a related angle to this: beyond the repeated permission prompts, the macOS dialog itself is confusing because it displays only the raw version number (e.g. "2.1.202") with no indication this is Claude Code, Claude, or Anthropic. Users who don't recognize the version string as their own dev tool have no way to identify what's requesting access, which reads as suspicious/unidentified software rather than an expected update from a known app. Even short of the stable-launcher-path fix, embedding CFBundleDisplayName/CFBundleName (e.g. "Claude Code") in the binary's Info.plist so TCC has something human-readable to show would resolve this independently.