[BUG] macOS: auto-update deletes the running app bundle mid-session, revoking granted Full Disk Access until restart

Status Open
Reported on v2.1.181
Maintainer reply None cached
Activity 4 comments · opened Jun 28, 2026

Summary

On macOS, when Claude Code auto-updates while a session is running, the updater deletes the currently running version's application bundle from ~/Library/Application Support/Claude/claude-code/<version>/claude.app. The live process is left executing from a bundle that no longer exists on disk. macOS then stops honoring the Full Disk Access grant for that process, and all file operations on TCC-protected locations (Downloads, Documents, Desktop, iCloud Drive) begin failing with Operation not permitted — silently, mid-session, with no error identifying the cause. A full quit and relaunch restores access.

Environment

  • Claude Code running under Claude Desktop
  • Version transition observed: 2.1.181 → 2.1.187 (auto-update during an active session)
  • macOS 27.0 (build 26A5368g), Apple Silicon (arm64)
  • Inner bundle identifier: com.anthropic.claude-code; host app: com.anthropic.claudefordesktop (Team Q6L2SF6YDW)
  • Full Disk Access had been granted to Claude and working for ~120 days prior

Steps to reproduce

  1. Grant Claude Full Disk Access. Confirm a Claude Code session can read a protected folder (e.g. ls ~/Downloads).
  2. Keep that session running.
  3. Allow (or trigger) a Claude Code auto-update to a new version while the session is still open.
  4. The updater removes the previous claude-code/<old-version>/ bundle.
  5. In the still-running session, run a file operation against a TCC-protected directory.

Actual behavior

  • ls ~/Downloads → Operation not permitted (EPERM). Same for ~/Documents, ~/Desktop, and iCloud Drive.
  • A non-protected directory (e.g. the project working directory) still reads fine.
  • The failure persists even with the per-command shell sandbox disabled — i.e. it is OS-level TCC, not the Claude Code sandbox or Claude Code's own permission layer (which were fully permissive).
  • The running process is executing from a bundle path that no longer exists on disk:
  • Process path: ~/Library/Application Support/Claude/claude-code/2.1.181/claude.app/Contents/MacOS/claude
  • On disk: only …/claude-code/2.1.187/ exists (created at the time of the update); the 2.1.181 directory returns "No such file or directory".

Expected behavior

  • An in-session auto-update must not delete the bundle of the running process.
  • Permissions granted to the running process (Full Disk Access) must remain in effect for the life of that process.

Impact

  • Silent, mid-session loss of file access on the user's primary folders; Read/Write tools begin failing.
  • Naturally misattributed by the user to their own macOS privacy settings (Full Disk Access appears granted, yet access fails), sending them to debug the wrong thing.
  • Any auto-update landing during an open session can reproduce it.

Suggested fixes

  1. Never delete the bundle of a running process. Defer removal of the old claude-code/<version>/ directory until no live process references it.
  2. Stage updates; activate on restart. Download into a new path but keep the running version's bundle intact for the life of the process; switch over on next launch.
  3. Run from a stable path (e.g. a …/claude-code/current symlink repointed atomically on restart) rather than executing directly from a version-stamped directory the updater later deletes.
  4. Detect and surface the condition. If the running bundle's on-disk path disappears, show a clear notice ("Claude Code updated in the background; file access is suspended until you quit and reopen") instead of letting operations fail as unexplained Operation not permitted.

Workaround

Fully quit Claude (⌘Q — not just closing the window) and reopen. A fresh process runs the intact, signed on-disk bundle and the existing Full Disk Access grant applies again.

Possibly related

  • #69546 — macOS: a hardcoded CLI version path breaks on every version bump (same version-stamped-path fragility).
  • #69140, #66997 — auto-update deleting user data on Windows (same "auto-update removes things it shouldn't" family).

View original on GitHub ↗

3 Comments

yurukusa · 3 months ago

Good, precise report — the quit-and-relaunch recovery you found is right, but here's the root mechanism and, more usefully, how to prevent it entirely so it never bites you mid-session.
Why the grant evaporates. macOS TCC ties a Full Disk Access grant to the code-signed bundle at a specific path. When the updater deletes claude-code/<old-version>/claude.app out from under the live process, that process is now executing from a bundle that no longer exists on disk. TCC can no longer match the running process back to the grant it issued (the path is gone, the inode is unlinked), so every subsequent access to a TCC-protected location (~/Downloads, ~/Documents, ~/Desktop, iCloud Drive) fails with Operation not permitted. A relaunch re-execs from the new bundle path, TCC re-matches, and access returns. So it's not a permissions reset — it's the grant being orphaned by the bundle deletion.
Prevention (the real fix until Anthropic stages updates safely): stop auto-update from running during a session. Either:

  • Set DISABLE_AUTOUPDATER=1 (or DISABLE_AUTOUPDATER=true) in the environment Claude Code launches with, and update deliberately between sessions; or
  • Pin updates to happen at quit rather than mid-session.

That removes the race entirely — the bundle is only swapped when nothing is executing from it.
Mid-session detection (so you don't waste an hour debugging "why did my file tools break?"): the signature is sudden Operation not permitted on TCC-protected paths only, while non-protected paths (e.g. a repo under ~/code) still work, and it appears right after a version bump. If file ops to ~/Downloads start failing but /tmp and your project dir are fine, suspect this and relaunch before assuming a code or config problem.
The deeper issue worth flagging to Anthropic: an updater that deletes the currently running version is unsafe on any platform — the standard pattern is to install the new version alongside and only retire the old bundle after the running process exits. The TCC revocation is just the most visible symptom of that.

Masaki0423 · 3 months ago

Adding independent confirmation of this issue with additional data.

Environment

  • macOS (Darwin 25.5.0)
  • Claude Desktop (com.anthropic.claudefordesktop)
  • Embedded Claude Code: 2.1.202 at ~/Library/Application Support/Claude/claude-code/2.1.202/claude.app/Contents/MacOS/claude
  • Symptom target: a local (non-iCloud) folder under ~/Documents (an Obsidian vault)

Observed behavior

  • Access to the target folder from both the embedded Claude Code (Cowork/Local Agent Mode background sessions) and from an independently-installed Claude Code CLI in a separate Terminal session (nvm-installed, different binary/path) intermittently fails while Claude Desktop is running. Fully quitting and relaunching Claude Desktop clears it — matching the workaround above.
  • lsof on the target directory shows zero open file descriptors held by any process at failure time — ruling out a plain file lock and pointing to an OS-level TCC/sandbox denial instead.
  • Querying TCC.db directly shows auth_value = 2 (Allowed) for kTCCServiceSystemPolicyDocumentsFolder for all relevant identities (com.anthropic.claudefordesktop, com.anthropic.claude-code, and the Terminal CLI's absolute path) — the grant looks intact in the database while access still fails in practice, consistent with this being silent and easy to misattribute to one's own privacy settings.
  • Multiple versioned directories accumulate under ~/Library/Application Support/Claude/claude-code/ across updates, consistent with the versioned-bundle-path mechanism here and in #63671 / #74234 / #70094.

Happy to share the full ps aux / TCC.db query output if useful.

armansh00 · 1 month ago

Still occurring on current builds, and I have two things to add that I think change the
diagnosis: the accepted workaround in this thread does not work, and the mechanism in
the comments above is wrong in a way that matters for the fix
.

Desktop 1.40609.0, sidecar 2.1.246 → 2.1.247, macOS Darwin 25.5.0, Apple Silicon.

1. DISABLE_AUTOUPDATER=1 does not prevent this

On the machine where this reproduced today, both are already in force:

$ env | grep DISABLE_AUTOUPDATER
DISABLE_AUTOUPDATER=1

$ jq '.autoUpdates, .autoUpdatesProtectedForNative' ~/.claude.json
false
true

It still happened. Those settings govern the desktop ShipIt updater, not the CCD
sidecar pruner that actually deletes the directory. Anyone applying the workaround in the
first comment is not protected.

2. It is not "the update swaps the bundle under a running process"

It is more specific, and more clearly a bug: on launch the app deliberately spawns new
work from the version it is about to delete.
From ~/Library/Logs/Claude/main.log, the
same triple every time — spawn from X, then delete X, 2–3 seconds later:

2026-08-16 19:02:51 [CCD] Falling back to installed version: 2.1.222
2026-08-16 19:02:54 [CCD] Installed bundle at .../claude-code/2.1.229
2026-08-16 19:02:54 [CCD] Removing old version: 2.1.222

2026-08-20 20:45:57 [CCD] Falling back to installed version: 2.1.234
2026-08-20 20:46:01 [CCD] Installed bundle at .../claude-code/2.1.235
2026-08-20 20:46:01 [CCD] Removing old version: 2.1.234

2026-08-26 01:08:29 [CCD] Falling back to installed version: 2.1.237
2026-08-26 01:08:34 [CCD] Installed bundle at .../claude-code/2.1.246
2026-08-26 01:08:34 [CCD] Removing old version: 2.1.237

2026-08-28 17:34:35 [CCD] Falling back to installed version: 2.1.246
2026-08-28 17:34:38 [CCD] Installed bundle at .../claude-code/2.1.247
2026-08-28 17:34:38 [CCD] Removing old version: 2.1.246

cleanupOldVersionsForTarget() keeps {justInstalled, buildPinVersion, targetVersion} —
there is no "has a live process" member. Its
.catch(... 'Could not remove old version %s (in use?)') guard reads like a Windows
assumption; on macOS unlinking a running executable succeeds silently, so it never fires.

Sixteen prunes are logged on this machine since 2026-05-12; only the four above bit,
because a prune only matters when a process is live from the doomed directory. A session
started 1–3 s after app launch lands squarely in the download window, so scheduled/background
tasks are the usual victims.

3. The failure is before signature validation, which is why TCC.db looks fine

The comments here say TCC "can no longer match the running process back to the grant."
It never gets that far. tccd fails at client-identity construction:

tccd: Failed to fetch responsible file descriptor: [2: No such file or directory]
tccd: proc_pidpath_audittoken() failed from PID[3420]: (#2) No such file or directory
tccd: ERROR: failed to construct a process for the responsible audit token for PID[5044].
tccd: Failed to create attribution chain from message.
sandboxd: Error Domain=kTCCErrorDomain Code=2 "missing 'auth_value' in reply message"
kernel: System Policy: Python(5044) deny(1) file-read-data /Users/.../CloudStorage/...

No signature is validated and no TCC.db row for any service is ever consulted — which
explains @Masaki0423's observation above that auth_value = 2 (Allowed) is present in the
DB while access still fails. The reply carries no auth_value at all and the kernel denies
by default.

Two consequences:

  • It is service-agnostic. sandboxd is logged asking kTCCServiceSystemPolicyAllFiles

first on every failing request and getting the same Code=2 before any service-specific
query. So granting Full Disk Access cannot fix it — the FDA row is behind the same
broken attribution chain. Worth stating explicitly, because FDA is the reflexive fix and
it costs people time.

  • The whole subtree dies. Claude Code is launched via Contents/Helpers/disclaimer

(posix_spawn disclaim), so the sidecar is the responsible process for every Bash/Python
descendant. One poisoned pid takes down everything the session shells out to.

4. Minimal reproducer — 30 seconds, no Claude involved, no update needed

cp /usr/local/bin/node /tmp/nodeD
/Applications/Claude.app/Contents/Helpers/disclaimer /tmp/nodeD <script reading ~/Documents>
#   -> tccd resolves it: AUTHREQ_SUBJECT subject=/private/tmp/nodeD, authValue=0
rm /tmp/nodeD
#   -> same process, same read, seconds later:
#      proc_pidpath_audittoken() failed from PID[n]: (#2) No such file or directory
#      Failed to create attribution chain from message.

The unlink is the single manipulated variable. This isolates the mechanism without waiting
for an auto-update.

5. Confirming an instance in the field

codesign -d --verbose=2 <claude-code-pid>   # "No such file or directory" on a LIVE pid == this bug
codesign -d --verbose=2 <parent-pid>        # control: resolves normally
/usr/bin/log show --last 5m --predicate 'process == "tccd"' | grep proc_pidpath_audittoken

Why this is worth un-staling

The failure is silent and mimics something else. It broke an automated daily task here four
times in twelve days, and each time it presented as a cloud-storage sync problem — because
~/Library/CloudStorage was the visible casualty while ~/Documents and ~/Downloads were
equally denied. stat() keeps working while open()/readdir() fail, which reads like file
corruption rather than a permissions problem.

--resume makes it materially worse: the app kills the old session, spawns a new process from
the doomed version, and resumes into it, so the transcript reads as continuous across the
process boundary. There is no visible seam where access was lost, and the successful file
writes from minutes earlier are from a different process — which sends you looking for a
mid-session revocation that never happened.

Retrying cannot recover it: the denial is cached in-kernel per pid and the running vnode has no
path. Only a brand-new process helps.

Suggested fixes, narrower than the ones above

  1. Exclude any version directory with a live process from the prune keep-set.
  2. Or: don't spawn a fallback session from the outgoing version when an install is already in

flight — wait for the new bundle. That alone closes the race in the logs above.

  1. Or: defer the prune until no process is running from that directory.

Happy to supply the full tccd/sandboxd unified-log extracts for any of the four dates.

Showing cached comments. Read the full discussion on GitHub ↗