[BUG] Background "bg" sessions permanently zombie after their owning window closes — daemon never marks them settled, visible as active in cross-window fleet view (macOS, 2.1.210)

Open 💬 0 comments Opened Jul 15, 2026 by andressrudqvist

Summary

Claude Code runs a single background daemon per machine (claude daemon run, tracked in ~/.claude/daemon.lock / daemon.log / daemon.status.json) that manages background ("template": "bg") sessions across all terminal windows and working directories, not just the one that spawned it. Any UI that lists "agents"/fleet across windows reads job state from ~/.claude/jobs/<id>/state.json.

When the terminal window/process owning one of these bg sessions is closed (or otherwise dies) while the job is mid-flight, the daemon never reaps it: state.json is left with "state": "working", and no corresponding bg settled <id> (done|killed) line is ever written to daemon.log. The job then persists indefinitely as an apparently-active agent in any cross-window agent/fleet view, with no live process behind it.

This looks like the same underlying defect class as #65423 (no timeout/reaping for agents that die mid-flight) but manifesting in a different subsystem: not a parallel Agent sub-agent stuck in one session's TUI switcher, but a whole daemon-tracked bg session orphaned at the cross-window/daemon level.

Environment

  • Claude Code 2.1.210
  • macOS (Darwin 25.3.0)
  • Multiple concurrent windows/sessions across three separate local checkouts of the same monorepo (repoA, repoB, repoC), all sharing one daemon (spawnedBy.cwd in daemon.lock shows the daemon was actually started from a third, unrelated window)

Evidence

Job 04e3d857 (cwd: repoB), compared against sibling jobs from the same daemon:

| Check | 04e3d857 | Sibling jobs (3cf8d140, 5eb4a370, 6fb75b2a) |
|---|---|---|
| state in state.json | "working" | "done" |
| updatedAt vs now | stale (2h+ with no further update) | consistent with a normal finish |
| Matching OS process (ps aux \| grep <sessionId>) | none | none (expected, they finished cleanly) |
| bg settled <id> (...) line in daemon.log | absent | present ((done) or (killed)) |
| daemon.status.jsonworkers | {} (daemon itself has no live worker for it) | n/a |

No ~/.claude/daemon.json index exists to cross-check against, so the only record of this job's state is the stale state.json file itself.

Repro (best-effort — root cause is inferred, not a guaranteed recipe)

  1. Start a background bg session from one window/repo (e.g. via a background Agent//loop/async session) and let it reach "state": "working".
  2. Close the terminal window/process that owns that session while it's still working (not via a graceful stop).
  3. Inspect ~/.claude/jobs/<id>/state.json from a different window: state remains "working", daemon.log never logs a settled line for that id, and no OS process matches the session id.
  4. Any agents/fleet view that aggregates jobs across windows still lists it as active.

Expected

The daemon should detect that the process backing a bg job has exited (regardless of cause) and mark it settled (done/killed) in state.json, so cross-window views never show a dead job as active.

Actual

The job is left in "state": "working" forever. Only manual filesystem cleanup (renaming/removing ~/.claude/jobs/<id>/) clears it; there is no user-facing way to reap it.

Impact

  • Cross-window agent/fleet views accumulate stale entries that look active, with no way to tell them apart from real in-progress work without inspecting the filesystem.
  • No programmatic recovery: the zombie job has no live process, isn't in any session's task list, and doesn't respond to TaskStop (which only resolves jobs known to the current session).

Related

  • #65423 — same defect family (no timeout/reaping for agents that die mid-flight), but at the in-session parallel-Agent/TUI-switcher layer on Windows, not the cross-window daemon-tracked bg job layer on macOS. Filing separately since the repro, evidence, and affected subsystem differ.

Suggested fix

  1. Daemon should periodically verify liveness (PID check) of every bg job it tracks and mark dead ones settled (e.g. killed) instead of leaving state.json untouched.
  2. Cross-window agent/fleet views should do their own liveness check before rendering a job as active, rather than trusting state.json verbatim.

Workaround

Manually inspect ~/.claude/jobs/*/state.json, cross-reference against daemon.log for a missing settled line and against ps aux for a missing process, then rename/remove the stale job directory.

View original on GitHub ↗