[BUG] Background daemon never reaps stale workers or unclaimed spares — they are re-adopted on every restart (64 leaked processes / ~7.1 GB after six weeks)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The background-session daemon never reaps stale workers or unclaimed spares. Worse, on every self-restart it re-adopts them, so the dead weight is inherited indefinitely rather than cleared. After six weeks of normal use I had 64 leaked claude processes holding ~7.1 GB RSS, none of which had done anything for 25+ days.
The daemon's own log shows it knows they are stale and declines to clean them up. From today, on 2.1.235, immediately after a routine upgrade restart:
[2026-08-19T05:24:49.885Z] [supervisor] binary at ~/.local/bin/claude changed (2.1.233 → 2.1.235) — self-restarting for upgrade
[2026-08-19T05:24:49.908Z] [supervisor] shutting down (cause=upgrade, uptime=170930s, leases=0, live_workers=32)
[2026-08-19T05:24:50.154Z] [supervisor] ─── daemon start ─── version=2.1.235 pid=70841 origin=transient
[2026-08-19T05:24:50.188Z] [bg] bg adopt: adopted=32 respawned=0 dead=0
[2026-08-19T05:24:50.191Z] [bg] bg orphan-spare reap: 1
[2026-08-19T05:25:05.208Z] [bg] bg: post-takeover prewarm burst — respawned 0/32 stale workers, 32 refused in 0s
adopted=32 ... dead=0 — the new daemon takes ownership of 32 workers and classifies none of them as dead. Fifteen seconds later it tries to respawn them, all 32 refuse, and it calls them "stale" in its own log message. Nothing then removes them. orphan-spare reap: 1 is the only reclamation that happens, and it reclaims a single process.
This is not new to 2.1.235 — the identical shape appears at every upgrade restart in my log going back weeks, e.g. 2.1.217→2.1.218 (adopted=33 respawned=0 dead=0, orphan-spare reap: 1, respawned 1/33 stale workers, 32 refused).
Breakdown of what leaked
64 processes, age ≥ 25 days, ~7.1 GB RSS:
- 22 processes = 11 wedged background session workers (a
bg-pty-hostplus its--session-id … --fork-session --resume …child), ~2.8 GB. Each reports toclaude agents --jsonasbackground / blocked / idle, i.e. still "active". Ten of the eleven never wrote a transcript at all; the eleventh wrote a 0-byte file. They are running binaries from 2.1.202–2.1.218 that have long since been superseded. - The remainder are pre-warmed spares and their pty hosts that were spawned, never claimed, and never released.
A worked example, traceable end-to-end in the log. Session 6ffcdf0e was spawned, and 17 minutes later an upgrade restart occurred:
[2026-07-22T05:40:40.860Z] [bg] bg spawned 6ffcdf0e (slash)
[2026-07-22T05:40:40.865Z] [bg] bg spare spawned host pid=92993
[2026-07-22T05:58:39.222Z] [supervisor] binary changed (2.1.216 → 2.1.217) — self-restarting for upgrade
[2026-07-22T05:58:39.472Z] [supervisor] ─── daemon start ─── version=2.1.217 pid=67516 origin=transient
[2026-07-22T05:58:39.472Z] [supervisor] ─── daemon start ─── version=2.1.217 pid=67499 origin=transient
[2026-07-22T05:58:39.486Z] [supervisor] another daemon won the lock race (pid=67499) — exiting
[2026-07-22T05:58:39.507Z] [supervisor] ─── daemon start ─── version=2.1.217 pid=67520 origin=transient
[2026-07-22T05:58:39.508Z] [supervisor] ─── daemon start ─── version=2.1.217 pid=67521 origin=transient
[2026-07-22T05:58:39.519Z] [supervisor] another daemon is already running (pid=67499, …)
[2026-07-22T05:58:39.531Z] [bg] bg adopt: adopted=34 respawned=0 dead=0
There is no bg settled 6ffcdf0e line anywhere in the log, before or since. It was adopted by the lock-race winner and never settled. Its process pair is still resident 28 days later.
Two things stand out in that excerpt beyond the leak itself:
- Four daemons are launched simultaneously and race for one lock. Three lose and exit. This is presumably benign by design, but it is the moment the worker's ownership is lost.
- A worker adopted during that handover can end up with no lifecycle owner — never settled, never respawned, never reaped, but still counted as an active background session by
claude agents.
Steps to Reproduce
- Use background agents regularly (
claude --bg, the Agent tool,/bg) across several repos, over a period of weeks, on a machine that stays powered on. - Let Claude Code auto-update a few times so the daemon performs its upgrade self-restart.
- Inspect the resident process set:
ps -axo pid,ppid,etime,rss,command | grep claude - Inspect
~/.claude/daemon.logforbg adopt:,orphan-spare reap:andpost-takeover prewarm burstlines.
Expected Behavior
- Workers that refuse respawn and are classified as stale during the post-takeover burst are terminated, not carried forward.
- A worker adopted during a daemon handover that never re-attaches to a client is eventually settled and reaped, and stops being reported as an active background session.
- Unclaimed pre-warm spares have a bounded lifetime.
Actual Behavior
Stale workers and unclaimed spares accumulate without limit and are re-adopted on every daemon restart. On this machine that reached 64 processes and ~7.1 GB, with 11 of them reported indefinitely as live background / blocked / idle sessions despite having written no transcript and being weeks dead. Manually killing them is currently the only remedy.
Environment
- macOS, Apple Silicon (arm64)
- Claude Code 2.1.235 (native CLI install under
~/.local/share/claude/versions/); leaked processes span 2.1.202 – 2.1.218 - Daemon is
origin: transientper~/.claude/daemon.lock - Usage pattern: many concurrent background agents across multiple repositories, machine rarely rebooted
Related
- #78454 — stale worker entries in
roster.jsonsurviving process death (the ledger-side counterpart; this report is the process side) - #87813 — background shell processes not cleaned up on session exit (different subsystem: Bash tasks, not daemon workers/spares)
3 Comments
Confirmed / reproduced on Linux, the stale-worker half: background daemon started from 2.1.233 with two
claude --bgsessions, then the binary replaced in place by 2.1.235. The daemon loggedbinary ... changed (mtime changed) — self-restarting for upgrade, thenbg adopt: adopted=2 respawned=0 dead=0,bg orphan-spare reap: 1, and 15 s laterpost-takeover prewarm burst — respawned 0/2 stale workers, 2 refused in 0s— the same signature as your log. The two 2.1.233 workers and their pty hosts survived that restart and a second forced one, still running the old binary, andclaude agents --json --allkept listing them as background / idle / blocked; nothing terminated or respawned them.Difference here: the unclaimed pre-warmed spare WAS reaped on each restart (
orphan-spare reap: 1and the spare process gone), so the spare-accumulation half did not reproduce on Linux in a short run. Also noticed while cleaning up:claude daemon stop --anystopped the daemon and workers but left thebg-pty-hostprocesses running.🤖 Generated with Claude Code
---
_Generated by Claude Code_
Same signature on Windows 11 (npm-global, 2.1.237), so this is not macOS/Linux-specific. Three additions below that may narrow the fix — in particular, a reaper does exist, it is just gated on the wrong condition.
Signature reproduces
dead=0every time, refused every time, nothing removed — identical to the OP.1. A reaper exists, but only memory pressure triggers it
This is the part I had not seen mentioned. The daemon can retire workers, and says so:
So the machinery is there; its trigger is just narrow on two axes — it fires only under memory pressure, and only for workers already
settled. The workers thatpost-takeover prewarm bursthas itself just classified as stale are never handed to it. Widening that one call site may be a smaller change than building a new reaper.2. Eviction is recorded as
settled (done), indistinguishable from successDirectly after that third line, with 94 MB free:
110 ms between spawn and
(done). The worker never ran; it was evicted so the next spawn could proceed. But the log records the same(done)a genuinely finished session gets, so nothing downstream can tell the two apart — the transcript is empty either way andclaude agentsreports it as finished.This has a practical consequence for anyone building cleanup tooling around this issue. A watchdog here treats
settled … (done)as "finished, do not touch" (deliberately — respawning genuinely finished sessions caused a revive loop previously). That rule is correct for real completions and silently wrong for evictions, and from the log alone the distinction cannot be made. A separate cause on the settle line (evicted/memory) would fix that for every consumer.Related: this compounds the "failed sessions shown under Completed" point in #83715 — here it is the log itself that loses the distinction, one layer below the view.
3. On Windows the leak can be unkillable and unreadable from the user's own session
Two leaked workers here were 60 h old, ~187 MB, in Windows session 0, and their supervisor's parent was long gone. The OP's remedy ("manually killing them") needs a qualifier on Windows: if the respawn that created them was issued from an elevated process — e.g. a Scheduled Task with Run with highest privileges, which is a natural place to put a watchdog — the workers inherit high integrity. From the user's normal (medium-integrity) session:
Stop-Process -Id <pid> -Force→Access is deniedOpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)→ error 5, so not even the image path is readableExecutablePathandCommandLinefor themSo they cannot be cleaned up, and cannot even be identified as Claude processes, without an elevated shell. Any documented cleanup step for Windows should say so. This also makes third-party cleanup filters that key on
ExecutablePathsilently no-op against exactly the processes that leaked.(The same integrity mismatch has a second effect worth knowing when placing a watchdog: while an elevated daemon holds the control pipe, the medium-integrity desktop app cannot reach it at all — UIPI blocks Medium→High — so
claude --bgfrom the app times out until that daemon is gone.)Environment
origin: transient; 4–9 concurrent background sessions, machine runs 24/7---
_Investigated with Claude Code._
Please note that the label
reproducedon this issue does not shield it from becoming stale and eventually auto-closed.To prevent this you would need to collect at least 10 thumbs up or comment every time before the 14 days stale interval expires.
See https://github.com/anthropics/claude-code/issues/87647 for details, and consider upvoting that for visibility.