Scheduled tasks leak headless claude.exe --resume processes on Windows (never exit, accumulate until OOM)

Status Open
Reported on v2.1.170
Maintainer reply None cached
Activity 8 comments · opened Jun 15, 2026

Summary

On Windows, a recurring scheduled task (created via Claude Code's scheduled-tasks feature) spawns a headless claude.exe --resume <id> --output-format stream-json worker on every fire. The worker finishes its work but never exits — it sits idle indefinitely. Over hours these accumulate until the machine runs out of RAM and freezes (hard reboot required). Still reproduces on 2.1.170.

This looks related to #58565 (closed as duplicate) and #42169 / #54130 (both closed "not planned"), but the trigger here is different — a scheduled task on a timer, not closing a session window or opening the panel — and it still happens, so filing fresh with current details.

Environment

  • Claude Code 2.1.170 (channel latest; embedded in Claude Desktop, no standalone CLI)
  • Claude Desktop 1.12603.1.0
  • Windows 11 Pro
  • Several MCP servers configured (Slack, Atlassian, Bitbucket, Postman, …)

Repro

  1. Create a recurring scheduled task (~7-minute cron) that runs a trivial step and stops (e.g. a watcher task that just runs a script via Bash).
  2. Leave it running for several hours.
  3. Watch the claude.exe process count and total memory climb.

Expected

Each scheduled run's headless process exits when the run completes.

Actual

Each run leaves a resident claude.exe --resume … --output-format stream-json --input-format stream-json --permission-prompt-tool stdio process:

  • ~95 orphaned workers accumulated over ~10 hours (one per ~7-min fire).
  • ~250–430 MB each; ~36 GB total across all claude.exe before the machine OOM'd and froze.
  • Each worker burned ~3–5 min of CPU (it did run the work) then sat at ~0% CPU forever — finished, then hung instead of exiting.
  • All parented to the Claude Desktop supervisor process.
  • Separately, 2,322 local_*.json session files (~622 MB) piled up under %APPDATA%\Claude\claude-code-sessions\… over ~3 weeks, never pruned.

Likely cause

The headless --resume worker blocks waiting for a stdin EOF the launcher never sends (no idle timeout in headless mode), so it never terminates. The configured MCP servers loaded on each run also keep connections/handles open, inflating each worker's footprint.

Impact

System-wide memory exhaustion → laptop freezes → hard reboot. It's silent (no UI), so it's easy to miss until the machine locks up.

Workaround

Disable the scheduled task and reap the orphans:

Get-CimInstance Win32_Process -Filter "Name='claude.exe'" |
  Where-Object { $_.CommandLine -like '*claude-code*--output-format stream*' -and ((Get-Date)-$_.CreationDate).TotalMinutes -gt 10 } |
  Stop-Process -Force

View original on GitHub ↗

7 Comments

yurukusa · 2 months ago

Your root-cause reasoning lines up with how headless --resume behaves: with --input-format stream-json, the worker keeps stdin open waiting for more newline-delimited input and only tears down on EOF. A timer-launched worker that finishes its turn has nothing left to read but never receives the EOF (the launcher doesn't close the pipe), and there's no idle timeout in that mode — so it parks at ~0% CPU holding its heap (plus the MCP connections loaded that run). The per-run accumulation you measured is the expected outcome of that, not a separate leak.
One thing worth flagging on your reaper before you leave it running: the TotalMinutes -gt 10 filter keys off process age, which also matches a worker that is legitimately still doing work on a long run. If any scheduled task ever takes >10 min, an age-only sweep will Stop-Process -Force it mid-flight — trading a memory leak for silent truncated runs. A safer signal is "finished, then parked": sample CPU twice and only reap workers whose CPU time stopped advancing while age is past your run's normal ceiling. Roughly (Windows; please verify on your box, I'm reasoning from the repro rather than a Win11 machine here):

$cand = Get-CimInstance Win32_Process -Filter "Name='claude.exe'" |
  Where-Object { $_.CommandLine -like '*claude-code*--output-format stream*' }
$t1 = $cand | ForEach-Object { [pscustomobject]@{ Id=$_.ProcessId; K=$_.KernelModeTime; U=$_.UserModeTime } }
Start-Sleep 30
foreach ($p in $t1) {
  $now = Get-CimInstance Win32_Process -Filter "ProcessId=$($p.Id)" -EA SilentlyContinue
  if ($now -and ($now.KernelModeTime + $now.UserModeTime) -eq ($p.K + $p.U)) { Stop-Process -Id $p.Id -Force }  # no CPU progress in 30s = parked, safe to reap
}

That only kills workers that burned no CPU across the sample window — an actively-working run is spared.
For the second pile you noticed — the 2,322 local_*.json (~622 MB) under %APPDATA%\Claude\claude-code-sessions\ — there's no pruning, so it grows forever. Until that's fixed, a safe age-based sweep that keeps recent sessions:

Get-ChildItem "$env:APPDATA\Claude\claude-code-sessions" -Recurse -Filter local_*.json |
  Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) } | Remove-Item -Force

And since the leak is silent until OOM, the most reliable mitigation until there's an idle timeout in headless mode is to make the reap self-healing rather than manual: register the CPU-delta sweep above as its own small recurring task (every ~15 min). That way orphans never accumulate to the OOM point even if you forget, and it won't touch a run that's still working.
The real fix is upstream — an idle/EOF-less timeout on --resume … --input-format stream-json workers so a finished headless turn exits on its own, plus retention/pruning on the session-file directory. This is the same headless-worker-never-exits family as #42169 / #54130 / #58565, but you're right that the timer trigger is a distinct path and still reproduces on 2.1.170, so it's worth keeping open on its own.

khenderson-asurity · 2 months ago

Note: the analysis below was written by Claude Code (Anthropic's assistant), running on the affected machine — I'm posting it on its behalf as the issue author. The diagnosis and command-line details are its findings, not mine.

Following up with empirical detail on the current behavior (still 2.1.170, Windows 11):

Confirmed the leaked workers do carry --resume. After a ~7-minute recurring scheduled task had been running, the only claude.exe processes on the machine with --resume were the scheduled-task workers themselves:

claude.exe --resume <id> --output-format stream-json --input-format stream-json --permission-prompt-tool stdio …

They park at ~0% CPU holding their heap (plus that run's MCP connections) and never exit — exactly the "stdin never gets EOF / no idle timeout" path @yurukusa described. Notably, the interactive session did not carry --resume, so on this build --resume actually marks the leaked workers, not the live session (worth knowing if anyone scripts a cleanup — keying off "no --resume" misses them).

Workaround that sidesteps it entirely (for the "run a script on a timer" case): don't schedule it as a Claude Routine — let the OS scheduler run the script directly. On Windows, launching it via pythonw.exe from Task Scheduler spawns no headless claude.exe at all, so nothing leaks (and no console window flashes each run). That only applies when the scheduled job is a plain script that doesn't need Claude itself.

Agreed the real fix is upstream: an idle/EOF-less timeout on --resume … --input-format stream-json workers so a finished headless turn exits on its own.

vancejason · 2 months ago

Corroborating on Windows 11 with the bundled claude-code 2.1.181, so this still reproduces past 2.1.170.

Data point: recurring scheduled tasks (routines) accumulated 32 resident ...\claude-code\<ver>\claude.exe workers over ~2 days, ~6.9 GB total. Start times map 1:1 to hourly-ish fire times (:00/:03/:07). Each finishes its work, drops to ~0% CPU, and never exits.

Second-order impact: a finished-but-resident worker can keep holding the global scheduled_tasks.lock. A task sharing that cron slot then loses the lock race and is silently rolled forward without running (lastRunAt stays stale), so the leak also makes scheduled runs skip, not just OOM.

Safe workaround (daily scheduled PowerShell reap of prior-day workers only):

$today = (Get-Date).Date
Get-Process -Name claude |
  Where-Object { $_.Path -like '*\claude-code\*\claude.exe' -and $_.StartTime -and $_.StartTime.Date -lt $today } |
  Stop-Process -Force

Filtering on \claude-code\ avoids the WindowsApps desktop Claude.exe tree; < today avoids killing the live/interactive session.

Expected fix remains: each scheduled headless run should exit when its work completes.

cameronkeng · 1 month ago

Same accumulation on 2.1.219, but with two differences from the original report that may point at a second code path — recording them here rather than opening a duplicate.

Environment: Windows 11 Home (10.0.26200), Claude Code 2.1.219 via the desktop app, 32 logical cores, 63.8 GB RAM. Two recurring scheduled tasks fire daily (07:35 and 07:57).

The accumulation reproduces exactly. Every scheduled-task firing leaves one headless claude.exe --output-format stream-json session resident after the run completes and writes its output. Observed across four consecutive days — both daily tasks park a session every time, and nothing reaps them:

| | day 1 | day 2 | day 3 |
|---|---|---|---|
| claude.exe processes | 28 | 34 | 48 |
| total working set | ~5.1 GB | 9.42 GB | 13.97 GB |
| sessions older than 24h | 15 | 15 | 25 |
| free RAM (of 63.8 GB) | — | 27.9 GB | 20.6 GB |

Oldest resident session at the time of writing: 90.8 hours. The only thing that has ever cleared them is a restart (or killing them by hand, which is what I ended up doing — 50 processes / 14.12 GB down to 26 / 6.34 GB).

Difference 1 — no --resume

The original report describes claude.exe --resume --output-format stream-json workers. I checked the full command line of all 35 resident sessions on this machine: zero carry --resume, and zero carry --continue. They are plain --output-format stream-json --input-format stream-json desktop-app sessions. So the leak is not specific to the --resume path.

Difference 2 — they are not idle

The original report says the leaked workers "sat at ~0% CPU forever". These do not. Sampling every parked session's UserModeTime + KernelModeTime twice over a 30-second window:

  • 25 of 25 parked sessions advanced their CPU counters. None was flat.
  • Aggregate burn: 43.7% of one core (~1.4% of a 32-core box), heaviest single session 5.2%.

So these are not hung at exit — they are sitting in some polling loop indefinitely. That is a meaningfully different end state from "finished, then hung", and may be the more useful thread to pull: something is keeping the event loop alive rather than the process failing to tear down.

Other things checked, in case they narrow it

  • Not orphans. All resident sessions have a live parent claude.exe (one parent process owned 29 of them). The app is holding them, not leaking detached children.
  • Session transcripts persist to ~/.claude/projects/<cwd-key>/<session-uuid>.jsonl, so the resident process is not the only copy of the conversation. Useful for anyone hitting this: you can restart the app without losing conversation history, only warm context. Verified — transcript count and total size were identical before and after killing 25 sessions.
  • No user-facing mitigation exists. There is no session-TTL or cleanup option in settings.json to turn on.

Happy to pull anything else off this machine — it reproduces daily without any effort on my part.

tnguyen007-debug · 29 days ago

Still present in Claude Desktop 1.24012.9 / bundled Claude Code CLI 2.1.219 (Windows 11 Pro, build 26200).

Data from ~15.5 h of app uptime with two scheduled tasks (one every 20 min, one hourly):

  • 65 stale headless workers accumulated — exactly one per completed run, like clockwork
  • ~5.8 GB working set held; commit charge climbed to 54 GB of a 63.7 GB limit (system was heading for resource exhaustion)
  • Each worker: child of the Desktop app process, launched from %APPDATA%\Claude\claude-code\2.1.219\claude.exe --output-format stream-json --input-format stream-json ...; consumed real CPU during its run, then sat permanently idle — consistent with the OP's stdin-EOF diagnosis
  • All corresponding sessions showed as finished in the app. Force-killing the stale workers reclaimed everything (commit 54 → 24 GB) with no data loss — transcripts intact, session list intact, subsequent scheduled runs unaffected

One finding that may help others writing workarounds (and possibly the fix itself): headless scheduled-run workers always carry --disallowedTools AskUserQuestion on their command line; interactive session hosts never do. That allows a precise reaper that cannot touch interactive sessions:

Get-CimInstance Win32_Process -Filter "Name='claude.exe'" | Where-Object {
  $_.CommandLine -match 'claude-code' -and
  $_.CommandLine -match '--output-format stream-json' -and
  $_.CommandLine -match 'disallowedTools\s+AskUserQuestion' -and
  ((Get-Date) - $_.CreationDate).TotalMinutes -gt 60
} | Stop-Process -Force

We run this hourly via Task Scheduler as a stopgap; happy to share the full script.

+1 for an idle timeout in headless mode, or closing the worker's stdin when the run's completion handler fires.

hrachika · 24 days ago

Confirming this on macOS as well — it is not Windows-specific.

Setup: Claude desktop app (bundled claude-code 2.1.221) with a scheduled task on a 5-minute cron. Each run spawns a headless worker (claude --output-format stream-json --input-format stream-json --permission-prompt-tool stdio ...) that completes its work but never exits.

Observed: 78 leaked processes accumulated over ~7.5 hours (one per scheduled run, none ever exited). Unlike the idle-RSS case in the OP, each lingering worker also kept burning ~3–6% CPU indefinitely, so together they held ~4 cores — enough to push kernel_task thermal throttling to ~300% and make the machine hot and barely usable until they were killed.

Notes:

  • Plain SIGTERM cleanly terminates the leaked workers.
  • Interim workaround: the scheduled job itself now reaps sibling workers older than 10 minutes at the start of each run.

Environment: macOS (Darwin 25.6.0), claude-code 2.1.221, desktop app 1.25927.0.

qoekuh · 14 days ago

Note: the analysis below was written by Claude Code, running on the affected machine; I'm posting it on its behalf. Same convention as @khenderson-asurity's comment above.

Reproduces on macOS — this is not platform:windows

Same bug, same mechanism, on macOS 26.6.1 (Apple M2 Max, 32 GB) with claude-code 2.1.229 / Claude Desktop 1.30096.5 — newer than any version corroborated so far in this thread. Suggest widening the platform:windows label.

Three recurring tasks (two hourly, one */30) = 4 fires/hour. Over ~22 hours:

| | |
|---|---|
| Resident claude workers | 90 |
| Combined RSS | 23.4 GB |
| Load average (1-min) | 138.84 |
| kernel_task | 120.8% |
| Free RAM | 44 MB |
| VM compressor | 14.5 GB |
| Swap | 7.2 GB of 8.2 GB |

macOS degrades differently from the Windows freeze: instead of OOM it thrashes the VM compressor, so kernel_task pins a core doing nothing but compression and the machine becomes unusable while never actually running out. Easy to misdiagnose as thermal throttling.

Caught one live: finished in 18 seconds, resident 24 minutes later

PID 2235  knowledge-base-howto-monitor
  06:32:16  process start
  06:32:34  final assistant message written to transcript   <- work complete, 18s
  06:56:50  still resident, 362 MB, 0% CPU, idle in kevent64

Another that finished at 06:03 was still alive 24 hours later. Every transcript from the killed batch ends with a proper final summary plus a last-prompt terminator — these runs all succeeded. Nothing is hung at the task level.

Direct confirmation of the stdin-EOF mechanism

@yurukusa's reasoning above is correct, and it's directly observable on macOS. fd 0 of a lingering worker is a still-connected unix socket back to the desktop app, not a closed pipe:

$ lsof -p 2235 -a -d 0,1,2 -nP
claude  2235  ...  0u  unix 0x916c9a42b3b326ad  ->0x295b3a2a193120f2
claude  2235  ...  1u  unix 0xc73570cb88f27d3f  ->0xe318067174975f71
claude  2235  ...  2u  unix 0x2ae46c974e8b7b32  ->0x13da8658751ef061

sample shows the process parked in kevent64 under the main thread — an idle event loop with a live handle, exactly as predicted. The launcher (Claude.appContents/Helpers/disclaimerclaude) never closes that socket, so the worker never sees EOF.

Platform difference worth flagging: on macOS the leaked workers do not carry --resume

This inverts the Windows discriminator. @khenderson-asurity confirmed the Windows leaked workers do carry --resume. On macOS it's the opposite — checked across four concurrent processes:

pid=2235   --model claude-sonnet-5   NO-RESUME     <- leaked scheduled run
pid=2933   --model claude-opus-5     --resume=1a20b3f5-…   <- real session
pid=97938  --model claude-opus-5     --resume=4fdfdda2-…   <- real session
pid=97969  --model claude-fable-5    --resume=77880575-…   <- real session

Anyone porting the PowerShell workaround from this thread to macOS will kill their live sessions if they filter for --resume. Scheduled runs here are spawned fresh with no session to resume; interactive ones are always resumed.

For anyone building a reaper: process age and CPU are both unsafe filters

@yurukusa already flagged that age-based filters catch legitimately-busy workers. Worth adding that CPU usage doesn't separate them either — the idle event loop still ticks. Measured over 40 s:

| process | CPU delta | actually |
|---|---|---|
| leaked worker | 0.37 s | idle, finished 20+ min ago |
| working session | 0.50 s | mid-task |
| working session | 0.52 s | mid-task |

0.9%/s vs 1.3%/s is far too close to threshold on.

The signal that does work is transcript write recency. A working agent appends to its ~/.claude/projects/<cwd>/<uuid>.jsonl on every message and tool call; a finished one never writes again. The worker's transcript can be matched to its PID by birth time (stat -f %B within ~90 s of process start — the leaked run's transcript was born 1 second after the process). Backstop it with a child-process check to cover a single long-running tool call, where there are no transcript writes but there is a child.

Impact

This is silent and there's no UI surface for it — the first symptom is the machine becoming unusable. Users who rely on scheduled routines and leave the machine on overnight will hit it every time. An idle timeout in headless stream-json mode, or simply closing the child's stdin once the run's final result has been emitted, would fix it.

Showing cached comments. Read the full discussion on GitHub ↗