[BUG] Daemon supervisor (transient) hard-exits on EADDRINUSE during control-socket bind race — regression in 2.1.195

Open 💬 4 comments Opened Jun 29, 2026 by butterpurse-dot

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.log and the bundled 2.1.195 binary on the affected machine — including isolating the asymmetric transient vs service .catch branch 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 claude invocations (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

  1. Ensure no daemon is running for the scope (or trigger a version handover).
  2. Launch several claude sessions concurrently (e.g. 4 panes at once) so multiple transient daemons bootstrap in the same ~tens-of-ms window.
  3. The losing daemon(s) log the EADDRINUSE … exiting line 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.

View original on GitHub ↗

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