[BUG] Daemon supervisor (transient) hard-exits on EADDRINUSE during control-socket bind race — regression in 2.1.195
Daemon supervisor (transient) hard-exits on EADDRINUSE during control-socket bind race — regression in 2.1.195
Attribution: This bug was identified, root-caused, and written up by Claude Code itself (Claude Opus 4.8), through forensic analysis of~/.claude/daemon.logand the bundled 2.1.195 binary on the affected machine — including isolating the asymmetrictransientvsservice.catchbranch and the 2.1.185→2.1.195 regression window. Reported on behalf of the operator.
Summary
On Linux, a transient (on-demand) daemon supervisor crashes with a fatal EADDRINUSE when it loses a race to bind() the control socket — instead of attaching to the already-running daemon or retrying. The sibling service code path handles the same failure gracefully (degrades, keeps registry workers). The transient path does not. This is a regression: the same machine/workload ran 2.1.173 → 2.1.185 for ~2 weeks with zero such crashes, then crashed 16 times in 3 days immediately after 2.1.195 was installed.
Environment
- Claude Code 2.1.195 (Bun standalone, ELF x86_64), installed via native installer to
~/.local/share/claude/versions/. - Linux 7.0.11 (Pop!_OS / Ubuntu base), single user (uid 1000).
- Workload: many concurrent
claudeinvocations (multiple tmux panes, background/loop sessions, fleet automation) → frequent concurrent transient daemon bootstraps and version handovers. This is the trigger; ordinary single-session use will rarely hit it.
Exact crash signature (~/.claude/daemon.log)
Every occurrence has the same shape — a successful bind by the winning daemon, then ~10s later the losing daemon’s fatal exit (all daemons share one log file):
[bg] bg: control socket bound at /tmp/cc-daemon-1000/<scope>/control.sock
[bg] bg adopt: adopted=5 respawned=0 dead=0
[bg] bg: skipped post-adopt sweeps + roster rewrite — daemon.lock is held by pid <X> (yield/handover in flight)
[supervisor] bg manager failed to start: [EADDRINUSE] listen EADDRINUSE: address already in use /tmp/cc-daemon-1000/<scope>/control.sock — control pipe unavailable; exiting
(yield/handover in flight) is present in the majority of crashes → strongly correlated with daemon handover and concurrent cold-start windows.
Root cause (from the bundled JS)
The bg-manager startup promise rejects with EADDRINUSE; its .catch branches asymmetrically on daemon origin:
.catch((J) => {
if (dNm(TEt(J)), s.aborted||x||v||N||A||C||k) return;
let oe = `${on(J)?`[${on(J)}] `:""}${...message...}`;
if (r === "service") { // service: GRACEFUL
d.write("supervisor", `bg manager failed to start: ${oe} — control pipe unavailable; bg sessions disabled (registry workers keep running)`);
pNl(y).catch(ke);
return;
}
// transient: FATAL ← the bug
d.write("supervisor", `bg manager failed to start: ${oe} — control pipe unavailable; exiting`);
process.stderr.write(`bg manager failed to start: ${oe}\n`);
I = !0; // fatal flag
D?.(); // tear down supervisor
})
A transient daemon that loses the bind race should treat EADDRINUSE as “another daemon already owns this scope” — i.e. take the same benign path as the pre-bind guard (another daemon is already running … an on-demand daemon never displaces a running one) and exit cleanly, or connect to the existing control socket as a client / retry briefly. Hard-exiting the supervisor means the client that spawned it loses its background manager.
Reproduction
- Ensure no daemon is running for the scope (or trigger a version handover).
- Launch several
claudesessions concurrently (e.g. 4 panes at once) so multiple transient daemons bootstrap in the same ~tens-of-ms window. - The losing daemon(s) log the
EADDRINUSE … exitingline above rather than attaching to the winner.
Frequency here: 16 crashes between 2026-06-27 and 2026-06-29; first crash 4 minutes after 2.1.195’s first run; 0 crashes across the prior 6 versions over the preceding ~2 weeks in the same log.
Suggested fix
In the transient .catch, treat EADDRINUSE on the control-socket bind as a benign “lost the race” condition: connect to the existing socket as a client (or retry with backoff for the handover window), falling back to a clean non-fatal exit — mirroring the service branch and the existing pre-bind guard. Optionally unlink a stale socket (no live listener) before bind to cover the killed-daemon case.
Impact
Users with concurrent/automated multi-session workflows get intermittent loss of background sessions and supervisor restarts. Low visibility for single-session users; high for heavy/automated use.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗