Scheduled tasks and background sub-agents leak processes/UI state without cleanup

Status Fixed / completed
Reported on v2.1.119
Maintainer reply None cached
Activity 7 comments · opened Apr 29, 2026 · closed Jun 16, 2026

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.exe OS processes had exited (verified via Get-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)

  1. 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.
  1. 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.
  1. 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.
  1. 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.

View original on GitHub ↗

6 Comments

github-actions[bot] · 4 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/48312
  2. https://github.com/anthropics/claude-code/issues/53732
  3. https://github.com/anthropics/claude-code/issues/53641

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

xiaosmalls · 4 months ago

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 disclaimer helper:

  • Outer: /Applications/Claude.app/Contents/Helpers/disclaimer (Mach-O, parent = main Claude.app)
  • Inner: ~/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 stays S (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 — nextRunAt advances past missed slots without ever creating a child process, and no error surfaces in logs or UI. The cron-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/disown in any skill — grep returns zero). No Stop hooks configured. The leak signature is identical regardless of skill, which rules out user code.

Local workaround. Launchd agent reaping claude-code/2.1.121 processes 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-code 2.1.121, macOS Darwin 25.1.0.

erickcoser · 3 months ago

+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_background with an until [ … gh pr view … ]; do sleep 25; done loop. The subagents exited cleanly; their bash tasks survived.

By session end:

  • Tasks panel showed 11 "Running" bash tasks (descriptions like "PR 576 CI checks completing", "Wait for CodeQL", "PR 578 CI check transitions", etc.)
  • ps aux on the host showed zero matching bash processes — they had actually exited
  • lsof on the task output dir showed only the actively-running shell (mine)
  • pstree of 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 TaskStop from the parent on the bash task IDs (extracted by grepping the subagent transcript JSONL files in /private/tmp/claude-502/.../tasks/<agent-id>.output for Monitor started (task <id>) markers). Every TaskStop returned No 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:

  1. Real OS process: dead
  2. Subagent task registry: orphaned (subagent gone, registry persists)
  3. Parent task registry: never knew about them
  4. Harness UI display: stuck on "Running"

Suggested fix shape

When a subagent exits (clean or watchdog-killed), its task registry should be either:

  • Inherited by the parent (so parent's TaskStop can reach them), OR
  • Bulk-killed at subagent exit (since the subagent is gone, its background tasks have no consumer for their output anyway)

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_ms AND a "stop after N consecutive gh errors" counter. The N-failures path catches the post-merge case where gh pr view returns errors that the polled mergeStateStatus condition 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.

Awshux · 3 months ago

+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.exe helper 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 + ~10 python.exe MCP servers as children (filesystem, puppeteer, github, brave-search, pandoc, openfec, weather, time, google-docs, etc.) plus one conhost.exe per server.

Direct evidence of the top-of-hour spawn pattern

Leaked helpers, identified by Get-CimInstance Win32_Process filtered to claude.exe children of the main hub PID:

PID    ParentPID  Started
127908  23384  5/20/2026 10:01:15 AM
 66464  23384  5/20/2026 11:01:16 AM
136080  23384  5/20/2026 12:01:16 PM
 58412  23384  5/20/2026  6:00:19 PM
 91596  23384  5/20/2026  7:00:20 PM
 56680  23384  5/20/2026  8:00:20 PM
112004  23384  5/20/2026  9:00:21 PM
 21596  23384  5/20/2026 10:00:22 PM
 58696  23384  5/20/2026 11:00:22 PM
 48876  23384  5/21/2026 12:00:22 AM
 87552  23384  5/21/2026  1:00:23 AM
 47440  23384  5/21/2026  2:00:23 AM
 41340  23384  5/21/2026  3:00:24 AM
... (continues hourly through 8:00 AM)

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.exe helpers + 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 Compression also released 5.2 GB after the pressure eased.

Notes adding to the existing analysis

  • On Windows the leak is more visible than on macOS because of the conhost.exe fan-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.
  • The MCP servers themselves clean up correctly when their parent claude.exe exits. The lifecycle defect is purely in the routine-helper claude.exe, not in the MCP servers.
  • User-side mitigation while waiting for a fix: reduce routine cadence (I moved hourly → 3x daily), and run a process-tree kill targeting claude.exe children of the main hub that match the routine schedule. At 3x daily a 64 GB machine still fills in roughly two weeks without intervention.
ubiest · 3 months ago

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

  • Claude Desktop 1.8555.2
  • Bundled claude-code 2.1.149
  • macOS 26.5 (25F71), Apple Silicon, 24 GB RAM

What happened

A scheduled task (via the scheduled-tasks MCP, short cron) accumulated backend claude processes 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)

  • Kill reason for ~1,062 processes: vm-compressor-space-shortage
  • Compressor holding ~18.7 GB at the time of the kill
  • ~294 live claude backend processes in the snapshot — each ~190–200 MB RSS / ~160 MB real phys_footprint, all active, median CPU ~49 min, oldest ~1.6 h

It'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.app via the disclaimer helper.

Workaround for other macOS users until this is fixed

A launchd watchdog that counts backends every few minutes and gracefully restarts Claude Desktop past a threshold prevents the system-wide OOM:

ps -axo command | grep -F -- '--output-format stream-json' \
  | grep -E 'claude-code/[0-9].*/MacOS/claude' | grep -vc 'disclaimer\|grep'

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 .ips reports if they'd help triage.

MaxLeiter · 2 months ago

Thank you for reporting, this will be fixed in the next release

Showing cached comments. Read the full discussion on GitHub ↗