[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.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Confirming this regression is still present in 2.1.196 and 2.1.197 (not just 2.1.195) — observed continuously over 24+ hours on Linux (Arch/CachyOS).
The daemon supervisor restarts roughly every ~52s, with multiple candidate processes racing for
control.sock. Most cycles resolve cleanly (one wins, others loganother daemon won the lock race — exiting), but periodically (4 times observed in a single 24h window) all candidates fail to bind:This produces a genuine gap with no daemon running at all, surfacing to the user as the CLI's "Background service is still starting — try again in a moment" for 30-60s. Reproduced in both plain local interactive sessions and
claude --remote-controlsessions, so it's not mode-specific.Note: the roster.json-clearing workaround discussed over in #68146 does not fix this, and in our case triggered a separate, more destructive bug — see my comment there for details.
Cross-link: this looks like the socket half of #68146 (Linux — transient daemon
displaces itself every ~52s while
claude agentsis open). I hit your exactEADDRINUSEsignature on 2.1.197 and commented it there before finding this issue.The two are very likely one defect: the ~52s displacement is what generates the bind
race, and your asymmetric
.catchfinding is why the loser dies instead of degrading.Operator-visible shape, in case it helps reproduce: when a displacing supervisor wins
the lock but cannot bind the control socket the predecessor still holds, no supervisor
holds a reachable control pipe. The agent view then errors on every session click until
a later on-demand spawn clears the orphaned socket and rebinds. Workers stay healthy
throughout (
bg adopt: adopted=N respawned=0 dead=0), so background jobs keep runningand only the view appears broken — which makes the window easy to misattribute.
Possibly the same family on other platforms: #72233 (macOS Apple Silicon, ~52s respawn)
and #72898 (Windows, stale
daemon.lock+ PID reuse).**Please don't close this if it stops reproducing after 2.1.199 — the trigger was removed, the asymmetric branch was not.** Following up on my cross-link above with 2.1.205 findings, including a correction to something I got wrong.
The generator of the bind race is gone as of 2.1.199
The ~52s transient-daemon self-displacement (#68146) is what manufactured this race at high frequency, and it stopped at the 2.1.198 → 2.1.199 boundary. Production bisect from my
daemon.log— the last storm restarts run on 2.1.198 at 52.3s intervals, and the next supervisor comes up on 2.1.199 and never displaces again:Every supervisor since terminated on
cause=upgradewith a multi-hour lifetime (max 70.2h). Consistent with 2.1.199's changelog entry about the Linux daemon killing itself every ~50 seconds. On my machine theEADDRINUSEcrash last occurred 2026-07-01T20:48Z and has not recurred across 2.1.199 → 2.1.205 (~7 days).That is exactly why I'm commenting rather than staying quiet: fewer displacements means fewer concurrent binds, so this issue will look fixed without the reported defect having been touched.
The asymmetric branch is still in 2.1.205, byte-for-byte
Both sides of the
.catchyou isolated are still present, unchanged, in 2.1.203, 2.1.204 and 2.1.205 —servicelogs and returns, keeping registry workers; anything else logs and exits:So the guard that would make a transient supervisor survive a lost bind still isn't there.
Correcting myself: "unlink the stale socket before bind" is already implemented
On #68146 I suggested the successor was inheriting an orphaned socket file and that unlink-then-bind would fix it. That was wrong, and I'd rather retract it here than let it send anyone down the wrong path. I probed 2.1.205 directly against an idle instance:
| Condition on the control-socket path |
origin=transient|origin=service||---|---|---|
| Orphaned socket file, no listener | binds OK | binds OK |
| Socket held by a live listener | binds OK | binds OK |
In all four cases the log shows
[bg] bg: control socket bound at ...and the supervisor stays up. The bind path already removes an existing socket first, so neither a stale file nor a live holder is sufficient to produceEADDRINUSEon its own.What that leaves
EADDRINUSEtherefore requires a genuine TOCTOU between two daemons in the unlink→bind window: A unlinks, B creates and binds, A binds and getsEADDRINUSE— and A, being transient, takes the exit branch instead of degrading. The window still opens routinely. On every version handover my box starts several transient daemons within tens of milliseconds of each other:Most losers exit at the lock before they ever reach
bind(), which is why this is rare rather than constant — but the lock and the bind are not ordered with respect to each other, which is precisely how you got 16 crashes in 3 days when the storm was feeding contenders in continuously.Honest limits on this report
I could not reproduce the crash on 2.1.205, and I'm not claiming it still fires. My reachability argument is inferential: the exit branch is present, and the concurrent-bootstrap window that feeds it is still observable. What I can state as fact is that the storm that used to drive it is fixed, that the two single-daemon paths into
EADDRINUSEare closed, and that the transient/service asymmetry at the heart of this report is untouched as of 2.1.205.Suggested framing for a fix, unchanged from the original report: make the transient branch degrade the way the service branch already does —
bg sessions disabled, registry workers keep running— rather than exit. Cheap, and it makes the remaining race benign whether or not the window is ever closed.