[Bug] macOS native auto-updater deletes running binary, breaking agent/tool spawning with ENOENT

Resolved 💬 3 comments Opened Apr 13, 2026 by KinohTaGo Closed Apr 16, 2026

Summary

The macOS native installer's auto-updater deletes version binaries from ~/.local/share/claude/versions/ without checking whether a running process holds them as its process.execPath. Since Bun/Node snapshots process.execPath at startup and uses it for child process spawning, any long-lived session loses the ability to spawn subagents, run hooks, or invoke internal tools once the updater removes the binary — all fail with ENOENT: posix_spawn.

The binary autoUpdatesProtectedForNative exists as a key in both ~/.claude.json and the CC binary itself (confirmed via strings), suggesting this protection was designed but is not functioning on macOS.

This is a consolidated report with forensic evidence. Related issues: #18019 (canonical, stale-closed), #32352 (agents, stale-closed), #25364 (Linux, dup-closed), #41602 (open, symlink revert), #7547 (open, fs_usage unlink trace).

Steps to Reproduce

  1. Install Claude Code via native installer (curl -fsSL https://cli.claude.com/install.sh | sh)
  2. Start a long-lived session (e.g., agent teams, looping tasks, or simply leave a session open overnight)
  3. Wait for the auto-updater to download a new version and clean up old versions
  4. Attempt any operation that spawns a child process: TeamCreate/Agent, hooks, /compact, internal tool invocations

Expected Behavior

  • The auto-updater should not delete a binary that is the process.execPath of a running process
  • Alternatively, CC should re-resolve its binary path (e.g., via ~/.local/bin/claude symlink or PATH lookup) before spawning children, rather than using the stale snapshotted process.execPath
  • The autoUpdatesProtectedForNative setting (present in ~/.claude.json and the binary) should provide this protection on macOS

Actual Behavior

  • The updater unconditionally removes old version directories/files from ~/.local/share/claude/versions/
  • The parent process continues running via POSIX open-unlink (inode held in memory), but child spawning fails:

``
ENOENT: posix_spawn '/Users/<user>/.local/share/claude/versions/2.1.92/claude'
``

  • The cleanup action runs reactively within ~1 minute of detecting stale versions (observed: symlinks created at 17:44 were deleted by 17:45)
  • autoUpdatesProtectedForNative: true in ~/.claude.json has no observable effect — the binary is deleted regardless

Evidence

1. Four processes running on deleted binaries (forensic snapshot)

ps + lsof output collected on a live system (2026-04-11):

| PID | Started | process.execPath | On-disk status |
|-------|------------|-----------------------------|----------------|
| 87508 | Apr 2 21:25 | .../versions/2.1.90 | Deleted (inode 34764024 unreachable) |
| 30550 | Apr 2 20:04 | .../versions/2.1.90 | Deleted (inode 34764024 unreachable) |
| 7846 | Apr 4 14:18 | .../versions/2.1.92 | Deleted (inode 35144402 unreachable) |
| 94868 | Apr 6 18:24 | .../versions/2.1.92 | Deleted (inode 35144402 unreachable) |

The longest-running process had been executing on a deleted binary for 9 days. find -inum confirmed the inodes were unreachable from any directory entry.

2. Cleanup action timing (1-minute reactivity)

| Time | Action | Result |
|-------|--------|--------|
| 17:44 | User creates symlinks for versions 2.1.90–2.1.99, 2.1.102 pointing to 2.1.101 | Symlinks exist |
| 17:45 | Auto-updater cleanup runs | All symlinks deleted within ~1 minute |

The cleanup uses numeric version comparison — entries with version numbers lower than the current version are removed regardless of whether they are symlinks or real files.

3. autoUpdatesProtectedForNative key exists but is ineffective
# In ~/.claude.json:
"autoUpdatesProtectedForNative": true

# In binary (strings extraction):
"autoUpdatesProtectedForNative" — string present in the Bun single-binary

This key appears to be a designed-but-broken protection mechanism. Setting it to true does not prevent the updater from deleting the running binary on macOS.

4. Binary architecture detail

The version binary (e.g., versions/2.1.101) is a single 192 MB Mach-O arm64 file — a Bun static build with JS sources linked in. It is not a directory. This means the cleanup action's unlink directly removes the executable.

Platform Comparison

| Platform | Behavior | Why |
|----------|----------|-----|
| macOS | Running binary deleted, ENOENT on spawn | POSIX allows unlink of open files |
| Windows | Update fails with "file in use" error (#45400) | NTFS mandatory file lock prevents deletion |
| Linux | Same as macOS (#25364) | POSIX semantics |

Windows users are incidentally protected by NTFS file locking — not by an explicit fix. macOS and Linux users have no protection.

Workarounds

1. Disable the auto-updater (recommended)
# In ~/.claude/settings.json:
{
  "env": {
    "DISABLE_AUTOUPDATER": "1"
  }
}

Then manually update with claude install when convenient. Note: DISABLE_AUTOUPDATER set via shell export may be overridden by settings.json's env block (the internal DB7() function uses Object.assign(process.env, settingsEnv)).

2. Protect the current binary with chflags uchg (macOS-specific)
chflags uchg ~/.local/share/claude/versions/$(claude --version | awk '{print $1}')

Tested: the installer creates new versions as separate files (new inodes) without touching the uchg-protected binary. This preserves the running session's execPath target while allowing new installations to proceed. Requires re-applying after each manual update.

Proposed Fix

Any of the following would resolve this:

  1. Guard cleanup: Before unlinking a version directory/file, check if any running CC process holds it as process.execPath (e.g., via lsof or a PID lockfile in the version directory)
  2. Re-resolve at spawn time: Instead of using the startup-snapshotted process.execPath, resolve the binary path through the stable symlink (~/.local/bin/claude) or PATH lookup before each child spawn — similar to the self-healing implemented for ripgrep in v2.1.101
  3. Make autoUpdatesProtectedForNative work: The key already exists in the codebase. Activating its intended behavior on macOS would be the minimal fix
  4. Deferred cleanup: Only clean old versions on next startup (when no process can be using them), not during a live session

Environment

  • macOS 15 (Darwin 25.4.0), Apple Silicon (arm64)
  • Claude Code installed via native installer (claude install)
  • Versions tested: 2.1.90, 2.1.92, 2.1.96, 2.1.98, 2.1.101, 2.1.104

Related Issues

| Issue | Status | Relationship |
|-------|--------|-------------|
| #18019 | Closed (stale) | Canonical report — Homebrew path, same root cause |
| #32352 | Closed (stale) | Closest match — macOS agents, 33 failures in one session, area:agents label |
| #25364 | Closed (dup→#18019) | Linux native installer, identical ENOENT path |
| #41602 | Open | Native installer symlink revert — same updater mechanism |
| #7547 | Open | fs_usage trace proving node process unlinks its own binary |
| #45400 | Open | Windows — shows NTFS file lock prevents the same issue |
| #36250 | Open | DISABLE_AUTOUPDATER doesn't suppress npm view on startup |

---

This report consolidates findings from filesystem forensics (lsof, find -inum, stat), binary analysis (strings on the Bun single-binary), upstream issue/release-notes review, and a controlled chflags uchg experiment.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗