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

Status Open
Reported on v2.1.195
Maintainer reply None cached
Activity 5 comments · opened Jun 29, 2026

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 ↗

4 Comments

github-actions[bot] · 2 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/68146
  2. https://github.com/anthropics/claude-code/issues/64028

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

DLANSAMA · 2 months ago

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 log another daemon won the lock race — exiting), but periodically (4 times observed in a single 24h window) all candidates fail to bind:

[supervisor] bg manager failed to start: [EADDRINUSE] listen EADDRINUSE: address already in use /tmp/cc-daemon-1000/<hash>/control.sock — control pipe unavailable; exiting

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-control sessions, 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.

0xdhx · 1 month ago

Cross-link: this looks like the socket half of #68146 (Linux — transient daemon
displaces itself every ~52s while claude agents is open). I hit your exact
EADDRINUSE signature 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 .catch finding 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 running
and 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).

0xdhx · 1 month ago

**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:

[2026-07-02T23:43:10Z] --- daemon start --- version=2.1.198 origin=transient   (+52.3s)
[2026-07-02T23:44:03Z] --- daemon start --- version=2.1.199 origin=transient   (+52.3s)  <- last one

Every supervisor since terminated on cause=upgrade with 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 the EADDRINUSE crash 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 .catch you isolated are still present, unchanged, in 2.1.203, 2.1.204 and 2.1.205 — service logs and returns, keeping registry workers; anything else logs and exits:

service   -> "bg manager failed to start: <err> — control pipe unavailable; bg sessions disabled (registry workers keep running)"   + return
transient -> "bg manager failed to start: <err> — control pipe unavailable; exiting"                                                + process exit

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 produce EADDRINUSE on its own.

What that leaves

EADDRINUSE therefore requires a genuine TOCTOU between two daemons in the unlink→bind window: A unlinks, B creates and binds, A binds and gets EADDRINUSE — 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:

[...T21:24:59.210Z] --- daemon start --- version=2.1.205 origin=transient
[...T21:24:59.236Z] --- daemon start --- version=2.1.205 origin=transient
[...T21:24:59.260Z] --- daemon start --- version=2.1.205 origin=transient

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 EADDRINUSE are 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.

Showing cached comments. Read the full discussion on GitHub ↗