Auto-update race in background daemon causes a session to hang permanently and enter an unbounded retry loop (macOS)

Open 💬 0 comments Opened Jul 8, 2026 by Azy02

Environment

  • Claude Code version: 2.1.204 (bug occurred while a session was running 2.1.202; daemon flapped through 2.1.150 and 2.1.204 during the incident)
  • OS: macOS 26.5.1 (25F80), Apple Silicon (arm64)
  • Install layout: ~/.local/bin/claude symlink → ~/.local/share/claude/versions/<version>, managed by a background claude daemon run process (control socket under /tmp/cc-daemon-501/<id>/control.sock) that supervises a pool of pre-warmed "spare" worker processes for background/subagent execution.
  • Usage pattern: several concurrent interactive claude sessions open in separate terminals/tabs at once (this is a normal, intentional workflow — heavy parallel use of subagents/background tasks), so several daemon-launching processes exist on the machine simultaneously.

Summary

A long-running interactive session (claude --resume, mid-conversation) froze permanently — completely unresponsive to keyboard input, no error shown, no further output — during a window where the shared background daemon self-updated. The daemon's own log shows a genuine race condition: two daemon instances started in the same millisecond, and the auto-updater briefly applied a six-week-old cached binary before correcting itself seconds later. The interactive session's Bash tool detected its persistent shell had been silently replaced (cwd reset) right at that moment, then the session never produced another turn. The process stayed alive for 20+ hours afterward, sitting in what looks like a silent unbounded retry loop against the API (70+ simultaneous connections, ~509 MB sent vs. ~2 MB received cumulative), fully unresponsive to Ctrl-C/typing, requiring a manual kill from outside the process to recover.

Evidence / timeline (from ~/.claude/daemon.log)

[2026-07-08T04:23:03.338Z] [supervisor] ─── daemon start ─── version=2.1.204 pid=84716 origin=transient
...(normal spare-pool activity for ~12 minutes)...
[2026-07-08T04:35:03.364Z] [supervisor] binary at /Users/azy/.local/bin/claude changed (.../2.1.204 → .../2.1.150) — self-restarting for upgrade
[2026-07-08T04:35:03.365Z] [supervisor] shutting down (cause=upgrade, uptime=720s, leases=2, live_workers=7)
[2026-07-08T04:35:03.592Z] [supervisor] ─── daemon start ─── version=2.1.150 pid=86313 origin=transient
[2026-07-08T04:35:03.598Z] [supervisor] ─── daemon start ─── version=2.1.150 pid=86311 origin=transient
[2026-07-08T04:35:03.618Z] [supervisor] another daemon won the lock race (pid=86313) — exiting
...
[2026-07-08T04:39:03.629Z] [supervisor] binary at /Users/azy/.local/bin/claude changed (.../2.1.150 → .../2.1.204) — self-restarting for upgrade
[2026-07-08T04:39:03.849Z] [supervisor] ─── daemon start ─── version=2.1.204 pid=86931 origin=transient

Note two daemon processes (86313, 86311) launched in the same millisecond, and the version resolved to 2.1.150 — a build from roughly six weeks prior to the incident — before self-correcting four minutes later. This means the mechanism that decides "the binary changed, restart" read a transient/inconsistent value during the race, rather than the lock protecting that read.

Within seconds of the second flap (04:39:03 → back to 2.1.204), the affected interactive session's transcript shows its Bash tool surfacing:

The file ... has been updated successfully.
...
Shell cwd was reset to /Users/azy

(The session's actual cwd was ~/Desktop/Colophon — the reset indicates its persistent shell/worker process was replaced out from under it by the daemon restart.) No assistant turn appears in the transcript after this point — the conversation goes permanently silent while the process itself remains running.

Impact

  • The interactive terminal becomes completely unresponsive: no keystrokes are processed, even though the process is confirmed to be the foreground process group of its tty (not a terminal/stty-state issue).
  • The process is not deadlocked at the OS level in the sense of a busy loop (sample shows the main thread mostly parked in kevent64), but the network layer shows what looks like a silent, unbounded retry loop: 70+ simultaneous ESTABLISHED HTTPS connections to the API host, ~509 MB sent cumulative vs. ~2 MB received, sustained for 20+ hours with no surfaced error and no backoff/cap that ever gives up or informs the user.
  • Recovery requires manually killing the process from outside (kill -TERM, which worked cleanly with no orphaned children) and re-running claude --resume. No conversation data is lost since the transcript is flushed to disk per turn — but there's no way to recover from inside the TUI, and no indication to the user of what went wrong.

Suggested areas to look at

  1. The auto-update "binary changed, restart" check appears to read the symlink target without the same lock that later arbitrates "who owns this daemon," so a concurrent updater race can cause a transient downgrade to be observed and acted on before self-correcting. Consider gating the version-changed detection on the same lock used for daemon ownership.
  2. When the daemon restarts for an upgrade, any in-flight interactive session with a live dependency on a worker owned by that daemon instance appears to have no reconnect/error path — it hangs indefinitely instead of surfacing a recoverable error.
  3. Consider a client-side circuit breaker (bounded retries with backoff, then a surfaced error) for whatever request loop is generating the sustained high-volume send/low-volume receive pattern observed here, instead of an apparently unbounded silent retry.

Repro steps

Not reliably reproducible on demand (it's a race that depends on multiple daemon-launching claude processes updating at the same moment). Happy to provide the full daemon.log and session transcript privately if useful for debugging.

View original on GitHub ↗