[Bug] Linux self-upgrade races npm postinstall chmod → daemon self-respawn dies with EACCES (posix_spawn), records install_failed

Open 💬 1 comment Opened Jul 14, 2026 by MatusAV

Summary

On Linux (npm-local install), the daemon's self-upgrade path races the npm postinstall's binary write vs. chmod +x. The supervisor watches the CLI binary's mtime; when a new version lands it self-restarts by posix_spawn-ing the new binary. But install.cjs copies the ~260 MB binary first and only calls chmodSync(dest, 0o755) afterwards. If the supervisor's mtime watcher fires inside that window, it execs a binary that is written-but-not-yet-executable → EACCES on posix_spawn. The daemon dies, its self-respawn fails, background workers get orphan-reaped, and .last-update-result.json is left at install_failed — even though the on-disk binary is fine once the postinstall's chmod completes.

This is almost certainly the root cause behind the symptom-only reports #74957 (linux install_failed, Errors: []) and the "bg workers reaped on daemon restart" reports (#74182 / #74085).

Environment

  • Platform: Linux (WSL2) — Ubuntu 26.04 LTS, kernel 6.18.33.2-microsoft-standard-WSL2
  • Claude Code: 2.1.208 (failure occurred upgrading 2.1.207 → 2.1.208)
  • Install method: npm-local (~/.claude/local/node_modules/@anthropic-ai/claude-code)
  • node v22.22.1, npm 9.2.0
  • Binary size: 259,820,344 bytes (~260 MB)

What happened (verbatim from ~/.claude/daemon.log)

[2026-07-14T03:09:25.445Z] [supervisor] binary at .../claude-code/bin/claude.exe changed (mtime changed) — self-restarting for upgrade
[2026-07-14T03:09:25.479Z] [supervisor] shutting down (cause=upgrade, uptime=126481s, leases=4, live_workers=2)
[2026-07-14T03:09:25.874Z] [supervisor] upgrade self-respawn failed to spawn: EACCES: permission denied, posix_spawn '.../claude-code/bin/claude.exe' — bg workers may be orphan-reaped ~60s after this process exits unless a client restarts the daemon (run `claude agents`)

.last-update-result.json was then left at:

{"timestamp":"2026-07-14T03:10:18.849Z","path":"npm-local","outcome":"failed","status":"install_failed","version_from":"2.1.207","version_to":"2.1.208","error_code":null}

Root cause

node_modules/@anthropic-ai/claude-code/install.cjs writes the binary before making it executable:

// copy/rename the ~260MB binary into place (non-atomic copyFileSync on the EXDEV/fallback path)
copyFileSync(src, dest)          // <-- dest mtime changes here
...
if (process.platform !== 'win32') {
  chmodSync(dest, 0o755)         // <-- executable bit set only AFTER (line ~137)
}

The long-running supervisor watches dest's mtime and treats any change as "new version → self-restart." Because the binary is ~260 MB, the copy is not instantaneous, and the chmod happens strictly after the write completes, there is a window in which dest exists (mtime already bumped) but is not yet executable. The supervisor fired its respawn 429 ms after detecting the mtime change and hit EACCES.

Why it's intermittent

Timing-dependent. In this install's history, 6/7 auto-upgrades self-restarted cleanly (2.1.204→205→206→207…); this is the 1 that hit EACCES (grep -c "self-respawn failed" daemon.log = 1). The window was widened by (a) a 35-hour daemon uptime with active leases (leases=4, live_workers=2) keeping the mtime watcher hot, and (b) the large binary lengthening the copy.

Impact

  1. The running daemon dies mid-upgrade and cannot respawn.
  2. Background agent workers are orphan-reaped ~60 s later.
  3. .last-update-result.json is stuck at install_failed (surfaces in the /status "Last update attempt: failed" banner) despite the binary being fine after chmod completes — the failure record does not reflect the healed state. (Related: #70608, #67634.)

Workaround / self-heal

Re-launching claude re-runs the install and the binary ends up correctly at 0o755; 2.1.208 then runs normally. Manual fix if it ever doesn't self-heal: chmod +x <.../bin/claude.exe>.

Suggested fixes

  • Order of operations in the updater: stage the new binary under a temp name, chmod 0o755 it, then atomically rename it into place. The supervisor's mtime watcher would then only ever observe a fully-executable file (rename is atomic; the intermediate non-executable state is never visible at dest).
  • Defensive respawn: before posix_spawn, verify dest is executable (access(dest, X_OK)) and, on EACCES/ENOEXEC, retry a few times with small backoff (the file becomes executable within a few hundred ms) instead of treating the first failure as terminal.
  • Don't cache a stale install_failed: re-check the actually-installed version before persisting install_failed, so a transient race doesn't leave a permanent "update failed" banner (overlaps with #70608 / #67634).

Related

  • #74957 — same symptom (linux install_failed, Errors: []) with no root cause; this is the likely cause.
  • #74182 / #74085 — "bg workers crash-loop / reaped on daemon restart" — the orphan-reap here is the same failure mode triggered by a failed self-respawn.
  • #70608 / #67634 — install_failed cached and never self-heals in the status banner.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗