Agents daemon orphans old bg workers after auto-update → EAUTH: attach rejected (control-key version skew)
What happened
After Claude Code auto-upgraded the background agents daemon from 2.1.168 → 2.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
- Start one or more long-running background agents.
- While they run, let the binary auto-update (e.g.
2.1.168→2.1.169), which restarts the daemon. - Run
claude agents. - Observe
EAUTH: attach rejected: this client didn't present the daemon control key. claude daemon statusshowsN 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
- Daemon worker re-key / handoff on version-change restart (preferred).
- Defer the daemon restart until no background workers are running (drain-then-upgrade).
- 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 from2.1.168) - macOS (Darwin 25.3.0)
- Multiple
claude agentswindows open across separate repos; 8 bg workers, 5 from the prior version.
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
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:
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.
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.
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-drainflag 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 drainto finish their work orkillall -9 claudeto 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.
Adding a reproduction + root-trace from a daemon-supervised multi-agent setup (in case it helps pin the lifecycle bug).
Interactive
claudesessions spawn background workers (Workflow / subagents) supervised byclaude daemon runand persisted inroster.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_workersrather thanleases, 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 --anyonly when no live interactive REPL or headlessclaude -pworker is present, and holds off (never reaps) whenpsliveness 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.Possibly related, same trigger (auto-update →
claude agentscan'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-permissionsplaced before a subcommand is parsed as a prompt rather than routed (e.g.claude --dangerously-skip-permissions daemon statusopens an interactive session instead of runningdaemon 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.