[BUG] Windows Desktop: completed runs' CLI processes are never terminated — every scheduled-task run leaks a persistent process with open stdio pipes and live API connections
Environment
- Claude Desktop 1.34493.1.0 (Electron 42.9.2), claude-code CLI 2.1.237
- Windows 11 Pro 10.0.26300
Summary
The Desktop app launches every run — scheduled-task runs especially — as a persistent interactive CLI process (--input-format stream-json --output-format stream-json, no prompt argument, no exit condition). When the run completes, the app marks the session not-running in its own session registry but never closes the child's stdio pipes and never terminates the process. In stream-json interactive mode the CLI waits on stdin indefinitely by design, so an un-reaped child never exits on its own. Every scheduled run therefore leaks one main CLI process, plus its child shells/MCP servers, plus its open TLS connections to the Anthropic API, until something external kills them.
Observed impact (one workstation, 2026-08-25)
- 61 lingering main CLI processes, all direct children of the app process; 60 of 61 had zero child processes left to wait on.
- A 12-hour-old leaked process still held its libuv stdio pipes to the app open (verified with Sysinternals Handle:
\Device\NamedPipe\uv\13-<appPid>anduv\14-<appPid>) and 2 ESTABLISHED TLS connections to the Anthropic API. The app side had accumulated 229 named-pipe handles from its leaked children. - Secondary pile-up: 426 cmd.exe/conhost.exe/node.exe descendants hung under the leaked sessions (one leaked session alone held 25 shell children).
- ~6.6 GB RAM recovered by manually killing confirmed-dead processes.
- Under the resulting sustained process/handle load, concurrent process launches on the machine began failing probabilistically with 0xc0000142 / STATUS_DLL_INIT_FAILED (System event log ID 26) — user-visible error dialogs, and in our case each failed launch was a silently-skipped hook script.
Root-cause chain (each link observed directly, not inferred)
- The app's per-session state file (
claude-code-sessions/.../local_<id>.json) recordsscheduledTaskId,cliSessionId, and timestamps — but no PID and no process-lifecycle state. The app has no record that an OS process belongs to a session, so nothing in it can ever reap one. - In stream-json interactive mode the CLI blocks on its stdin pipe forever, by design.
- Run finishes → app marks the session not-running → stdio pipes stay open → child waits forever → process, RAM, and API connections persist until killed externally.
This is not specific to any one task: every scheduled-task run leaks its run's process (verified across several distinct scheduled tasks the same day).
Suggested fix
On run completion, close the child's stdio pipes and/or terminate the process tree; record the child PID (plus process creation time, so PID reuse can't cause a wrong kill) in the session state file so orphans from a crashed app can be reaped on next start.
Workaround we deployed locally (for other affected users)
A Stop-hook that, for sessions whose state file carries a scheduledTaskId, spawns a detached watchdog which waits for 10 idle minutes, re-verifies process identity by executable name + creation FILETIME, then kills the leaked tree; plus a SessionEnd sweep of leftover descendants. Interactive sessions are never touched. This fully contains the leak but is compensating for lifecycle management the app itself should do.
Possibly related (different platforms/subsystems, same lifecycle-management family): #83218 (macOS MCP subprocesses survive app quit), #89110 (Linux orphans on quit).
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗