[BUG] scheduled-tasks MCP cron-spawned claude.exe never exits on Windows — accumulates leaked processes (~350 MB each)

Resolved 💬 2 comments Opened Jun 9, 2026 by cellison-hashbrowns Closed Jun 9, 2026

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 result event in stream-json, process hangs.
  • #24478 — closed as not planned (Fedora aarch64, ~10 min unresponsive).
  • #24481 — closed as duplicate (macOS M1, claude -p hangs).
  • #25629 — closed as duplicate (Linux Ubuntu, hang after result event 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 on 2.1.160)
  • Claude desktop app: 1.11187.4.0 (parent of every leaked process)
  • Invocation: mcp__scheduled-tasks MCP, cron 17,47 * * * * on a SKILL.md task

Reproduction

  1. Create a scheduled task with mcp__scheduled-tasks__create_scheduled_task — short cron, trivial SKILL.md.
  2. Let it fire a few times.
  3. 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 Wait state. Wait-reason histogram dominated by Unknown (Node libuv parked on GetQueuedCompletionStatus) + a few EventPairLow. 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 hung wsl, 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)

  1. Scheduler closes stdin on the child once the response stream is consumed. Cleanest. No CLI changes needed.
  2. Auto-exit after terminal result frame in stream-json mode when stdin has been written and is non-TTY. Could ship behind --exit-on-result to preserve current behavior for long-lived sessions.
  3. 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.exe idle > N minutes — same effect, no prompt edits needed.

Neither is a real fix; both are treating the symptom.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗