Agents daemon orphans old bg workers after auto-update → EAUTH: attach rejected (control-key version skew)

Status Fixed / completed
Reported on v2.1.168
Maintainer reply None cached
Activity 6 comments · opened Jun 8, 2026 · closed Aug 19, 2026

What happened

After Claude Code auto-upgraded the background agents daemon from 2.1.1682.1.169 mid-session, opening the agents window (claude agents) failed to attach with:

Couldn't attach — EAUTH: attach rejected: this client didn't present the daemon control key — update Claude Code (old client?)

The "update Claude Code (old client?)" hint is misleading: the CLI client was already on the latest version (2.1.169). The orphaned parties are the background workers spawned by the previous daemon (2.1.168), which are cryptographically locked to the old control key and can't authenticate to the freshly-restarted daemon.

claude daemon status confirms the skew:

version: 2.1.169
bg workers:   8 running (control.sock), 8 in roster.json
              5 from a different CLI version
holding this daemon open:
  8 bg workers running (daemon waits for them to settle)

So 5 of 8 workers are from the prior version and cannot be attached, even though they are still alive and doing work.

Expected

When the daemon restarts due to a version change while background workers are still running, it should either:

  • re-key / migrate existing workers to the new control key, or
  • gracefully drain old-version workers (let them finish, keep them attachable) before/while adopting the new key, or
  • at minimum accept attach from same-user workers it can identify, rather than hard-rejecting with EAUTH.

Actual

The new daemon generates a fresh control key, and any worker started by the old daemon is permanently un-attachable for the remainder of its life. The only known recovery is killall -9 claude, which destroys in-flight background agent work — unacceptable when long-running sessions are mid-task.

Why this matters

Auto-update is on by default, so this happens silently to anyone who keeps background agents running across an update window. The user is forced to choose between losing in-flight work (killall -9 claude) or living with a broken agents window until every old worker finishes.

Repro

  1. Start one or more long-running background agents.
  2. While they run, let the binary auto-update (e.g. 2.1.1682.1.169), which restarts the daemon.
  3. Run claude agents.
  4. Observe EAUTH: attach rejected: this client didn't present the daemon control key.
  5. claude daemon status shows N from a different CLI version.

Current workarounds (both poor)

  • killall -9 claude — kills the daemon and all workers, losing in-flight agent work.
  • DISABLE_AUTOUPDATER=1 — prevents the binary from swapping under running workers, but disables auto-update entirely and is opt-in/undiscoverable.

Suggested fixes

  1. Daemon worker re-key / handoff on version-change restart (preferred).
  2. Defer the daemon restart until no background workers are running (drain-then-upgrade).
  3. Make the error message accurate — point at "old background workers from a previous version" and suggest a non-destructive recovery, not "update Claude Code (old client?)".

Environment

  • Claude Code 2.1.169 (auto-upgraded from 2.1.168)
  • macOS (Darwin 25.3.0)
  • Multiple claude agents windows open across separate repos; 8 bg workers, 5 from the prior version.

View original on GitHub ↗

5 Comments

kcarriedo · 2 months ago

The version-skew scenario here is a real control-plane failure: after the daemon restarts, it generates a new control key, but background workers that were spawned by the old daemon still hold the old key — and there's no re-key path. So they're running but unreachable, and the "update Claude Code" hint makes things worse because you're already current.

Two questions that might help narrow the right fix:

  1. Do the orphaned workers continue making progress (i.e., are they still doing useful work during the window they're alive but unreachable), or do they stall because they can't phone home to the new daemon to fetch follow-on instructions?
  1. Is the control key bound to the daemon process (new key on every restart) or to the daemon version (same key for all 2.1.169 installs)? If it's per-process, then even a restart without an upgrade would orphan in-flight workers — which is a larger scope than the auto-upgrade case.

If it's per-process: the immediate mitigation is probably a grace window on daemon restart where the old key remains valid long enough for in-flight workers to checkpoint and either complete or hand off state to the new daemon. If it's per-version: then a re-key / adoption protocol (new daemon actively claims running workers from the prior version) is the path forward.

Either way, the misleading "update Claude Code" error message should be fixed now — a daemon-restart orphan should surface as "daemon restarted mid-session, some workers are from a previous daemon instance" rather than implying the client is stale.

dwemer8 · 2 months ago

Encountered the same issue after automatically upgrading from 2.1.168 to 2.1.169 on Ubuntu 22.04.5 LTS. Downgrading to 2.1.168 allowed me to open previous sessions, but they were full of "Request timed out" errors, and the terminal froze after opening a session.

kcarriedo · 2 months ago

The "drain-then-upgrade" approach you suggested is the right default behavior here. Silently restarting the daemon mid-flight while workers hold in-progress state is essentially a lost-update problem — the same class of bug that causes data loss in distributed systems when a coordinator restarts without flushing.

The cryptographic control key mechanism makes sense for security, but it needs to account for rolling upgrades. A few angles on the suggested fixes:

On re-key/handoff (option 1): This is the cleanest UX, but requires the old daemon to export worker metadata before exit so the new daemon can re-issue credentials. Feasible if worker state is already serialized somewhere (it seems like it is, given workers persist across tab close).

On drain-then-upgrade (option 2): Simpler to implement but adds latency to auto-updates for active users. Could be gated on a --no-drain flag for users who prefer the current instant-update behavior.

On the error message: Regardless of which fix lands, the message needs to change. "Update Claude Code (old client?)" is actively misleading when the client is current — it sends users on a wild goose chase. Something like "5 workers from a prior daemon version — run claude agents drain to finish their work or killall -9 claude to recover immediately" would be much clearer.

This is a good issue to watch; the background-worker model is promising for parallel workflows but this class of version-skew bug will be a recurring pain until the daemon handles versioned worker handoff.

XetsusDev · 2 months ago

Adding a reproduction + root-trace from a daemon-supervised multi-agent setup (in case it helps pin the lifecycle bug).

Interactive claude sessions spawn background workers (Workflow / subagents) supervised by claude daemon run and persisted in roster.json. On unclean session death those workers are orphaned; the daemon then adopts them across its own auto-upgrade restarts and respawns them on kill, so they accumulate over time (we observed one orphan still running ~19 h later). This matches the version-skew / "attach rejected" symptom in this issue — the adopted worker is from a prior control-key version.

Root cause appears to be that the daemon's on-demand exit gates on live_workers rather than leases, so orphaned workers keep the daemon alive instead of letting it (and them) wind down.

Workaround we deployed: a guarded systemd reaper that calls claude daemon stop --any only when no live interactive REPL or headless claude -p worker is present, and holds off (never reaps) when ps liveness can't be determined — so it can't kill live work. It clears the orphan set on a timer once the session that spawned them is gone. Happy to share specifics if useful.

muzerk · 2 months ago

Possibly related, same trigger (auto-update → claude agents can't reach the background service) but a different root cause worth ruling out here: #72643.

In that report the daemon fails to cold-spawn after the update because a bypass-permissions client can't spawn the supervisor — --dangerously-skip-permissions placed before a subcommand is parsed as a prompt rather than routed (e.g. claude --dangerously-skip-permissions daemon status opens an interactive session instead of running daemon status; flag-free works). This is distinct from the control-key version skew described here — no EAUTH, just no daemon, no control.sock, and nothing in daemon.log.

Cross-linking in case both are hit in the same claude agents + auto-update scenario.

Showing cached comments. Read the full discussion on GitHub ↗