[BUG] scheduled-tasks MCP cron-spawned claude.exe never exits on Windows — accumulates leaked processes (~350 MB each)
Summary
Every firing of a mcp__scheduled-tasks cron spawns a fresh claude.exe child of the Claude desktop app on Windows. After the model emits its final turn-complete frame, the process does not exit — it sits idle at ~350 MB indefinitely, holding the PID, handles, and working set. Over ~3 days at a 30-min cadence I accumulated 43 orphan processes / ~906 MB before manually killing them.
This is the same underlying bug as the closed-as-not-planned / closed-as-duplicate reports below, but with a different (and IMO worse) blast radius: the bundled scheduler is a first-party feature that turns this into an unbounded leak rather than a one-shot CI annoyance.
Prior reports (all closed)
- #1920 — closed as not planned. Canonical: missing final
resultevent in stream-json, process hangs. - #24478 — closed as not planned (Fedora aarch64, ~10 min unresponsive).
- #24481 — closed as duplicate (macOS M1,
claude -phangs). - #25629 — closed as duplicate (Linux Ubuntu, hang after
resultevent in stream-json).
Adjacent open scheduler issues:
- #66509, #66294, #66486, #66070.
None of the closed reports cover Windows, the scheduled-tasks MCP spawn path, or the desktop-app-as-parent case. Asking for these to be reconsidered, or for a narrower fix scoped to the scheduler's stdin handling.
Environment
- OS: Windows 11
- Claude Code:
2.1.165(also reproduced on2.1.160) - Claude desktop app:
1.11187.4.0(parent of every leaked process) - Invocation:
mcp__scheduled-tasksMCP, cron17,47 * * * *on a SKILL.md task
Reproduction
- Create a scheduled task with
mcp__scheduled-tasks__create_scheduled_task— short cron, trivial SKILL.md. - Let it fire a few times.
Get-Process claude | Where Path -like '*claude-code*'— each firing leaves one process behind.
Diagnostic evidence
Captured from a freshly-leaked process ~2 minutes after the cron tick:
- Cumulative CPU: 5 s total, 0 active.
- All 28 threads in
Waitstate. Wait-reason histogram dominated byUnknown(Node libuv parked onGetQueuedCompletionStatus) + a fewEventPairLow. Structurally identical to an interactive session sitting at the prompt — the only difference being that no one is going to feed this one any more stdin. - No real children — only
conhost.exe. No hungwsl,kubectl,bash, etc. ParentProcessId= the desktop app's main process.- Command line (
Win32_Process.CommandLine):
````
claude.exe --output-format stream-json --verbose --input-format stream-json
--model default --permission-prompt-tool stdio
--allowedTools mcp__computer-use,mcp__ccd_session__*
--disallowedTools AskUserQuestion
--setting-sources=user,project,local --permission-mode default
--include-partial-messages --plugin-dir <...>
--replay-user-messages --settings {}
Notably: --input-format stream-json with no exit-on-result flag, and the parent never closes stdin after the prompt frame.
Likely cause
The scheduler writes the prompt as stream-json to the child's stdin, consumes the response stream, then keeps stdin open. Without EOF, claude.exe's libuv event loop has nothing to drain and parks waiting for the next frame indefinitely. Process is structurally identical to an interactive REPL — except no user exists.
This matches the symptom #1920 describes ("process never receives end signal") but on a different code path: the scheduler is on the writer side, and even when the final result frame is emitted to stdout, the child has no way to know stdin is done.
Side effect: scheduled-tasks.json unbounded growth
Possibly downstream of the same root cause: the scheduler's recordedSkips accumulates one entry per minute per task whenever it skips a fire due to per_task_limit (which trips because it thinks the prior run is still alive — which, technically, it is, because the process is still running). I observed 1,599 stale entries / 128 KB before manual compaction. Reproducible from one session running for a few days.
State file path: %APPDATA%\Claude\claude-code-sessions\<session-id>\<sub-id>\scheduled-tasks.json.
Expected behavior
After the model's terminal frame for a stream-json turn, the process exits — either because the parent closes stdin, or because the child knows the turn is over and shuts the event loop down.
Suggested fixes (any one)
- Scheduler closes stdin on the child once the response stream is consumed. Cleanest. No CLI changes needed.
- Auto-exit after terminal
resultframe in stream-json mode when stdin has been written and is non-TTY. Could ship behind--exit-on-resultto preserve current behavior for long-lived sessions. - Idle timeout (e.g. 60 s of no input on stdin in stream-json mode → exit).
(1) is probably the highest-leverage fix because it's also what would stop recordedSkips from accumulating.
Workarounds in use
- Per-SKILL.md self-kill as the last step:
``powershell
$p = $PID
while ($p) {
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$p"
if (-not $proc) { break }
if ($proc.Name -eq 'claude.exe') { break }
$p = $proc.ParentProcessId
}
if ($p) {
Start-Process -WindowStyle Hidden powershell
-ArgumentList '-NoProfile','-Command',"Start-Sleep -Seconds 45; Stop-Process -Id $p -Force"
}
```
Detached + delayed so the assistant's final message + notifySessionId callback flush before the kill.
- Windows-side janitor scheduled task that kills any
claude-code\*\claude.exeidle > N minutes — same effect, no prompt edits needed.
Neither is a real fix; both are treating the symptom.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗