[BUG] claude binary re-registers as new app in macOS Privacy & Security on every version update (versioned install path, not code signing)

Open 💬 16 comments Opened Apr 16, 2026 by blouque

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

  1. Install Claude Code and grant Full Disk Access when prompted
  2. Open System Settings → Privacy & Security → Full Disk Access
  3. Confirm claude appears in the list with the toggle enabled
  4. Install any Claude Code update (version increment, any size)
  5. Attempt to use a feature requiring Full Disk Access (e.g., iMessage plugin)
  6. Observe: macOS prompts for Full Disk Access again
  7. Return to System Settings → Privacy & Security → Full Disk Access
  8. 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_

View original on GitHub ↗

16 Comments

github-actions[bot] · 3 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/48311
  2. https://github.com/anthropics/claude-code/issues/41297

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

Connoropolous · 3 months ago

this is a good idea. need this

blouque · 3 months ago

I see other duplicates but they keep getting closed out instead of actually getting addressed.

m13v · 2 months ago

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 reset per-service.

blouque · 2 months ago

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:

  1. A properly structured .app bundle at a stable, non-versioned path (e.g., /Applications/Claude Code.app/)
  2. A signed launcher stub at Contents/MacOS/ — this is the path TCC tracks
  3. The stub spawns the current versioned binary as a child process
  4. macOS walks the process tree and attributes resource access to the stable stub

Worth flagging: the current codesign output shows Executable=/Users/<user>/.local/share/claude/versions/2.1.111 — a raw Unix-style binary, not an .app bundle. 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 .app bundles + 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:

  • Full Disk Access (shown in screenshot)
  • Automation (required for the iMessage plugin — my original use case)
  • Accessibility
  • Screen Recording
  • Input Monitoring

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):

tccutil reset SystemPolicyAllFiles com.anthropic.claude-code

blouque · 2 months ago

Hoping this gets addressed soon. Happy to test anything.

dialethia · 2 months ago

Confirming this bug.

ccdwyer · 2 months ago

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.

glendigity · 2 months ago

Another here with the same bug.

nurikk · 2 months ago

same issue

jrignacio · 1 month ago

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:

5744  claude agents                                           ← terminal launcher
5751  claude daemon run --origin transient --spawned-by ...  ← daemon
5759  /.../.local/share/claude/versions/2.1.147 --bg-spare   ← background session (TCC-visible)
5758  /.../.local/share/claude/versions/2.1.147 --bg-pty-host ← PTY host paired with spare

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 .app bundle 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/claude would still leave the --bg-spare worker presenting as a raw versioned path.

Filed #61314 specifically to track the agent path. Linking here as the canonical issue.

ifrahdrordavid · 1 month ago

+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 fresh TCC.db row 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/claude repointed to ~/.local/share/claude/versions/<n> on each auto-update.

  • Built a tiny compiled (not shell) launcher at a fixed path, ad-hoc signed + hardened runtime, that execv()s the current version binary.
  • Granted it Full Disk Access once → landed as 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.159 directly | 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/claude itself 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 .app bundle) 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.

barkleesanders · 16 days ago

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_plist section. So TCC has no CFBundleName to show and falls back to the file name — which is the literal version string. The prompt reads:

"2.1.196" would like to access files in your Downloads folder.

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_plist section
  • displayed name = basename of the path = 2.1.196
  • signature is valid Developer-ID (Q6L2SF6YDW), so editing the binary to embed a bundle name would break it

The 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:

  1. Name the on-disk executable 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".
  2. Embed Info.plist (CFBundleName=Claude) in the Mach-O __TEXT,__info_plist at sign time → prompt shows "Claude".

Either makes the prompt recognizable as Claude instead of a bare number.

barkleesanders · 16 days ago

Correction on the orphan-row cleanup: tccutil reset SystemPolicyAllFiles com.anthropic.claude-code does not actually clear the accumulated version rows. Verified on 2.1.196 (macOS 26 / Darwin 25):

BEFORE: 15 rows where client LIKE '%/claude/versions/%'  (Downloads/Desktop/Documents/NetworkVolumes/MediaLibrary)
$ for svc in SystemPolicyAllFiles SystemPolicyDownloadsFolder SystemPolicyDesktopFolder \
             SystemPolicyDocumentsFolder SystemPolicyNetworkVolumes; do
    tccutil reset $svc com.anthropic.claude-code
  done
  → "Successfully reset ... approval status for com.anthropic.claude-code"   (all 5)
AFTER:  15 rows — identical. No change.

The "Successfully reset" message is misleading. These rows store the client as a path (.../versions/<ver>, client_type=1), but tccutil 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 of tccutil reset. The only thing that clears them is tccutil 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 / .app stub 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 current versions/<n> binary, CFBundleName=Claude embedded so the prompt reads "Claude") does carry an FDA grant across updates for the interactive claude path — matching @ifrahdrordavid's A/B result. It does not cover the claude agents / --bg-spare daemon path (@jrignacio), which still needs the upstream fix.

viktor1298-dev · 9 days ago

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.json already 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 overrides autoUpdates: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, signed Identifier=com.anthropic.claude-code, Team Q6L2SF6YDW, but Info.plist=not bound.
  • A stable bundle exists — ~/.local/share/claude/ClaudeCode.app (CFBundleIdentifier=com.anthropic.claude-code) — and Contents/MacOS/claude is the same inode as the versioned binary (the updater hardlinks it in). But the CLI shim points at the versioned path, not the bundle.
  • Process tree shows why granting FDA to the bundle doesn't help: the bundle-path parent re-execs a child at the versioned path, and that child is the process doing the file access:

``
ClaudeCode.app/Contents/MacOS/claude (stable bundle path, parent)
└─ versions/2.1.202 (versioned path, child — touches files → TCC keys FDA here)
`
Since TCC also resolves symlinks to the real path before checking, a stable
~/.local/bin/claude or a versions/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.

frinko · 7 days ago

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.