Scheduled tasks and background sub-agents leak processes/UI state without cleanup
Environment
- Claude Code: 2.1.119
- OS: Windows 11 Pro (10.0.26200) — observed on Windows; lifecycle/cleanup logic likely platform-agnostic
- Shell: Git Bash + PowerShell
---
Summary
A scheduled task or background sub-agent that completes its work successfully often does not exit cleanly. The harness lacks per-tool wall-clock timeouts, never SIGKILLs hung children, and leaves stale UI state with no way to clear it. Over a week these leaks accumulate to ~1.5 GB of zombied RAM and a Tasks panel cluttered with phantom "Running" entries.
---
Symptom 1 — Scheduled-task processes zombify after completing their work
A daily scheduled task (scheduled-tasks MCP, fires at 08:08) writes its output file successfully, but the underlying claude-code\2.1.119\claude.exe --output-format stream-json process never exits. After 4 days I had 4 zombied processes from the same daily task, each holding 200–600 MB resident.
A separate failure mode: one day the scheduled task hung mid-run (an MCP tool call apparently never returned). It produced no output at all and ran for 24+ hours. There is no per-tool wall-clock timeout to catch this — the child sits forever waiting on stdin from a parent harness that has long since lost interest.
Reproducer: create a scheduled task that calls a moderate set of MCP tools (the more servers in the chain, the higher the chance one stalls). After ~1 week, list claude.exe processes — expect to see one extra zombie per successful run, plus any hung mid-run.
---
Symptom 2 — Background bash tasks remain "Running" in the Tasks panel after their OS processes exit
I spawned a background sub-agent via the Agent tool (run_in_background: true) for a 10-minute pure-local audit. The sub-agent itself completed cleanly and emitted its <status>completed</status> notification. But during execution it had spawned several nested background bash commands (e.g. until [ -f flag ]; do sleep N; done polling loops). After the parent agent completed:
- ✅ All
bash.exe/sh.exeOS processes had exited (verified viaGet-CimInstance Win32_Process) - ❌ Five entries remained in the Tasks panel labeled "Running" indefinitely
Symptom 3 — No working path to clear the phantom entries
| Path | Result |
|---|---|
| Click X on the entry in the Tasks panel | No effect — entries persist |
| Call TaskStop with task ID derived from ~/AppData/Local/Temp/claude/<session>/tasks/<id>.output | Returns No task found with ID: <id> |
| Restart Claude Code | Works (clears panel) |
Whatever ID format the panel uses internally is not reachable from either the UI's own dismiss control or from TaskStop. Restart is the only escape.
---
Suggested fixes (in priority order)
- Per-tool wall-clock budget enforced by harness. If a single tool call doesn't return within N seconds (configurable, default ~120s), the harness should mark the call failed and feed an error back to the agent rather than letting the child sit forever on stdin. This alone would fix the silent-hang case.
- Hard exit-after-completion for scheduled tasks. When a
--output-format stream-json --resume <task-session-id>child emits its terminal turn message and stdin closes, the harness should SIGKILL after a short grace period instead of leaving the child alive.
- Tasks panel state should track underlying process exit. When a background bash task's OS process terminates, its panel entry should flip to "Completed" / "Failed" within seconds, not stay "Running" forever.
- Either fix the X button or align
TaskStop's ID lookup with the panel's IDs. Right now the user has no in-session escape from a stuck panel entry.
---
Evidence available on request: PowerShell Get-CimInstance output of zombied processes by age/RAM, full command lines showing the headless flags (--output-format stream-json --verbose --input-format stream-json --model default --permission-prompt-tool stdio --allowedTools mcp__computer-use,...), the smoke-test agent's transcript showing clean completion, and the temp-dir task ID file listing whose IDs TaskStop rejects.
Showing cached comments. Read the full discussion on GitHub ↗
6 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Filed #55607 before seeing this — closing mine as a dup but wanted to add evidence here that may help isolate the failure mode:
Paired-process fingerprint. Every leaked subprocess is paired 1:1 with a
disclaimerhelper:/Applications/Claude.app/Contents/Helpers/disclaimer(Mach-O, parent = mainClaude.app)~/Library/Application Support/Claude/claude-code/2.1.121/claude.app/Contents/MacOS/claude(parent = the disclaimer)The disclaimer wrapper is invoked because of
--allow-dangerously-skip-permissions. Both children survive past the skill's Stop event.Stuck on stdio EOF. The inner process is launched with
--input-format stream-json --permission-prompt-tool stdio. Both flags mean it reads stdin until EOF. State staysS(interruptible sleep) at ~80 MB RSS — the parent isn't closing the pipe when the skill completes, so the child reads forever.Silent dispatcher failure once the cap is hit. After ~24h at ~7 fires/hour I had ~190 stuck pairs accumulated. At that point the dispatcher silently stops spawning runners —
nextRunAtadvances past missed slots without ever creating a child process, and no error surfaces in logs or UI. Thecron-heartbeat-style watchdog pattern doesn't catch it because the heartbeat is itself one of the silently-skipped tasks. This is what makes the bug particularly nasty for long-running setups: scheduled automation just stops working invisibly.Confirmed not skill-side. Verified across multiple model IDs (
claude-opus-4-7,claude-sonnet-4-6,claude-sonnet-4-5-20250929), permission modes (bypassPermissions,default), and skill content (no&/nohup/disownin any skill —grepreturns zero). NoStophooks configured. The leak signature is identical regardless of skill, which rules out user code.Local workaround. Launchd agent reaping
claude-code/2.1.121processes whose elapsed time exceeds 30 min, every 10 min. Pings me if a single sweep reaps > 5, which happens about every 10 hours — gives a real-time gauge of the leak rate. Pinned to the version marker so it auto-no-ops on a fixed build.Build: Claude.app
1.5354.0/ claude-code2.1.121, macOS Darwin 25.1.0.+1 reproduction on macOS (Claude Code 2.1.121, macOS 15.x, zsh) — confirms the issue is platform-agnostic.
Specific failure mode: subagent-spawned bash monitors
In a long session running ~16 subagents in parallel (each shipping a PR), every subagent was instructed (via my parent prompt) to monitor its own PR's CI via
Bash run_in_backgroundwith anuntil [ … gh pr view … ]; do sleep 25; doneloop. The subagents exited cleanly; their bash tasks survived.By session end:
ps auxon the host showed zero matching bash processes — they had actually exitedlsofon the task output dir showed only the actively-running shell (mine)pstreeof the claude harness PID showed only 4 children (all MCP servers, no orphan bash)So: the bash subprocesses had terminated, but the harness-level task tracker never updated their state from "Running" to "Completed".
X button confirmed broken (cross-ref #48885)
User clicked the X next to each of the 11 zombies — none disappeared. So the kill path in the UI also doesn't reach the dead-but-tracked tasks.
TaskStop from parent session also can't reach them
I dispatched the subagents from a parent Claude Code session. After they exited, I tried
TaskStopfrom the parent on the bash task IDs (extracted by grepping the subagent transcript JSONL files in/private/tmp/claude-502/.../tasks/<agent-id>.outputforMonitor started (task <id>)markers). EveryTaskStopreturnedNo task found with ID: <id>.So the bash tasks live in the subagent's task registry, which gets orphaned when the subagent exits but isn't reachable by the parent's TaskStop tool either. Three layers of state inconsistency:
Suggested fix shape
When a subagent exits (clean or watchdog-killed), its task registry should be either:
Currently it's neither — they survive in a no-man's-land.
Workaround for prompt authors
When telling a subagent to monitor its own PR's CI, bound the polling loop with BOTH
timeout_msAND a "stop after N consecutivegherrors" counter. The N-failures path catches the post-merge case wheregh pr viewreturns errors that the polledmergeStateStatuscondition can't transition through, which is the most common reason these loops never self-exit.Happy to provide JSON dumps of the affected session's task registry if useful.
+1 — still reproducing on Claude Code 2.1.146 (released today, 2026-05-21) inside Claude Desktop 1.8089.1 on Windows 11 Pro build 26200. Three weeks since the original report; bug is not fixed.
Trigger
The built-in Routines feature configured for hourly cadence. Each fire spawns a fresh
claude.exehelper as a child of the main Claude Desktop hub. None of them exit after their routine completes.Accumulation after ~25 hours
| Process | Count | Total RAM |
| --- | --- | --- |
|
node.exe| 887 | 27.8 GB ||
python.exe| 753 | 9.8 GB ||
claude.exe| 45 | 8.4 GB ||
conhost.exe| 1,256 | 5.0 GB ||
Memory Compression| 1 | 6.8 GB || System RAM in use | | 53.5 / 63.9 GB (83.7%) |
Each leaked routine fire holds ~19
node.exe+ ~10python.exeMCP servers as children (filesystem, puppeteer, github, brave-search, pandoc, openfec, weather, time, google-docs, etc.) plus oneconhost.exeper server.Direct evidence of the top-of-hour spawn pattern
Leaked helpers, identified by
Get-CimInstance Win32_Processfiltered toclaude.exechildren of the main hub PID:Top-of-hour with ~20s drift — clearly a scheduled fire, not user activity. Every helper has the same ~29-process MCP descendant fan-out.
Cleanup confirms helpers were doing nothing
Killing the 27 hourly-spawned
claude.exehelpers + their entire MCP descendant subtrees (807 processes total) reclaimed 19.3 GB of RAM. The main Claude Desktop hub stayed alive and functional — confirming the orphaned helpers were idle and not serving any active role.Memory Compressionalso released 5.2 GB after the pressure eased.Notes adding to the existing analysis
conhost.exefan-out (one per MCP child) — 1,256 console host windows is itself a Task Manager red flag, even before you notice the underlying node/python count.claude.exeexits. The lifecycle defect is purely in the routine-helperclaude.exe, not in the MCP servers.claude.exechildren of the main hub that match the routine schedule. At 3x daily a 64 GB machine still fills in roughly two weeks without intervention.Still reproducing on the latest versions — and with the most severe outcome I've seen in this thread: on macOS this leak escalates past memory accumulation into a kernel-level, system-wide OOM (Jetsam) that jettisons ~1,000 processes.
Environment
What happened
A scheduled task (via the
scheduled-tasksMCP, short cron) accumulated backendclaudeprocesses overnight until the machine ran out of compressible memory and the kernel jettisoned ~1,000 processes — including system daemons (homed,callservicesd,accountsd,trustd, …). To the user this presents as "Claude keeps crashing the whole machine."Evidence (
/Library/Logs/DiagnosticReports/JetsamEvent-*.ips)vm-compressor-space-shortageclaudebackend processes in the snapshot — each ~190–200 MB RSS / ~160 MB realphys_footprint, allactive, median CPU ~49 min, oldest ~1.6 hIt's a per-session reaping leak, not runaway spawning
The scheduler state file shows only 23 logical scheduled runs that day (normal cadence) — yet 294 backends were resident. So each logical run leaves multiple backends behind rather than the cron over-firing.
The hang mode reproduces too
One scheduled run lasted 53.8 min (vs. ~11 min normal) and overlapped the next run, its backend never reaped — consistent with the "MCP tool call never returned + no per-tool wall-clock timeout" failure mode described in the original report.
Process fingerprint
Matches the earlier macOS comments: each leaked backend is
~/Library/Application Support/Claude/claude-code/2.1.149/claude.app/Contents/MacOS/claude --output-format stream-json --resume <id> …, parented by/Applications/Claude.appvia thedisclaimerhelper.Workaround for other macOS users until this is fixed
A
launchdwatchdog that counts backends every few minutes and gracefully restarts Claude Desktop past a threshold prevents the system-wide OOM:Reducing scheduled-task frequency slows the accumulation but does not stop it — the only real fix is reaping the backend when its session ends/hangs.
Happy to attach the full
.ipsreports if they'd help triage.Thank you for reporting, this will be fixed in the next release