[Windows] Stale daemon.lock + PID reuse after unclean shutdown deadlocks the transient daemon (every spawn logs "another daemon won the lock race" and exits)

Open 💬 0 comments Opened Jul 1, 2026 by seanmartinsmith

Environment

  • Claude Code 2.1.197 (native install), win32-x64, Windows 11
  • Background server mode: ephemeral (on-demand transient daemon)

Summary

After an unclean shutdown (a hard machine crash / power loss) while a background daemon held its lock, the daemon never recovers. Every subsequent transient-daemon spawn reads the stale ~/.claude/daemon.lock, sees the recorded PID as "alive," logs another daemon won the lock race (pid=NNNNN) — exiting, and quits without binding the control socket. Net effect: no working daemon, claude agents backgrounding fails, and opening the agents TUI crashes on the failed handoff. claude daemon status reports not running (so it's invisible), and claude daemon stop finds nothing to stop — so there's no obvious recovery path.

The twist that makes it permanent: the lock's PID got recycled by the OS to an unrelated process. In my case the dead daemon was pid 25200; after reboot Windows reassigned 25200 to an ordinary GUI application that launches at startup. So the daemon's "is the lock owner still alive?" check answers yes — but the PID now belongs to an unrelated user app, not a daemon.

Repro

  1. Have a background daemon running (open claude agents, or background a session) so ~/.claude/daemon.lock is written.
  2. Kill the machine uncleanly (power loss / bugcheck), or hard-kill the daemon process, so the lock is never released.
  3. Reboot such that the daemon's old PID gets reassigned to another long-lived process (common on Windows after a reboot).
  4. Try to open claude agents / background a session.

Observed

~/.claude/daemon.log — 13 consecutive spawn attempts over ~26 min, every one identical:

[..T17:49:37Z] [supervisor] ─── daemon start ─── version=2.1.197 pid=17508 origin=transient
[..T17:49:37Z] [supervisor] another daemon won the lock race (pid=25200) — exiting
...
[..T18:15:00Z] [supervisor] ─── daemon start ─── version=2.1.197 pid=27252 origin=transient
[..T18:15:00Z] [supervisor] another daemon won the lock race (pid=25200) — exiting

~/.claude/daemon.lock still pointed at the dead daemon:

{ "pid": 25200, "version": "2.1.197", "startedAt": <08:21 today>,
  "origin": "transient", "procStart": "639184908750139580", ... }

…but Get-Process -Id 25200an unrelated GUI application that had launched during startup (StartTime 13:48, i.e. right after the reboot) — not a Claude daemon. claude doctor showed control.sock: unreachable (ENOENT \\.\pipe\cc-daemon-*-control) and bg workers: 0 in roster.json (control unreachable).

Root cause

The lock staleness / "lock race" check trusts PID liveness alone. A dead daemon's PID that the OS has recycled to an unrelated process reads as "owner still alive," so every fresh supervisor defers and exits → permanent deadlock. Nothing ever reclaims the lock.

Suggested fix

The lock already records a procStart timestamp (and daemon.status.json carries supervisorProcStart, plus a launchTarget path). Before deferring to an existing lock, validate the live PID's actual process start time (and/or image path) against the recorded procStart/launchTarget; if they don't match, treat the lock as stale and reclaim it. Standard stale-lock / PID-reuse guard.

Workaround

Delete (or move aside) ~/.claude/daemon.lock and ~/.claude/daemon.status.json; the next spawn wins the freed lock and comes up clean (control socket binds, spare worker spawns).

⚠️ Do not use claude daemon stop --any in this state — if it acts on the recorded PID it can signal whatever unrelated process has since inherited that PID.

Related

  • #72334 (transient daemon hard-exits on EADDRINUSE during control-socket bind race) — same supervisor-lifecycle area, but a different failure point. That one races at socket bind; this one exits earlier, at the lock check, and is triggered by PID reuse after an unclean shutdown rather than a live race.

View original on GitHub ↗