Scheduled-task runner leaks finished stream-json sessions (never closes stdin / signals exit) → CPU exhaustion
What happened
A scheduled task (cron routine) firing every 20 minutes accumulated 136 idleclaude agent-mode processes over ~21 hours, driving system load to 70 and
CPU to ~85% (66% in kernel). Each leaked process holds a full MCP stack
(computer-use, markdown-vault, plugin MCPs), so the pile-up is heavy.
Root cause is a teardown bug in the scheduled-task ("rpm") runner: it launches
each routine as a persistent stream-json session and never reaps it after the
turn completes.
Diagnosis
The routine runner spawns sessions in persistent interactive mode:
claude --output-format stream-json --input-format stream-json \
--replay-user-messages --resume <id> ... (full plugin/MCP stack)
A stream-json session only exits on stdin EOF or a signal. After the
scheduled turn finishes, neither happens:
- stdin stays open, held by the desktop app. The leaked session's fd 0 is a
unix socket whose peer is owned by the Claude desktop app process — confirmed
via lsof. The app never closes it.
- The turn finished. Example process: 5m38s wall clock, 6.97s CPU total,
then idle.
- Idle in the event loop, not stuck working. Main thread parked in
kevent64 (libuv waiting for events). All leaked sessions show the identical park.
- MCP children keep handles alive.
markdown-vault-mcpand plugin MCP
servers remain attached as live stdio handles, so Node has no reason to exit.
Every 20-minute fire adds one more idle event-loop + MCP stack.
Proof it's the runner, not the routine's work
The routine's own sub-agents are launched one-shot
(claude -p ... --no-session-persistence) and exit cleanly every time. Only
the runner sessions the app spawns in persistent mode leak. The bug is in the
spawner's completion handling.
Expected
After a scheduled-task turn completes, the runner should close the session's
stdin (or send SIGTERM) so the process and its MCP children exit.
Suggested fix
Either:
- Launch routines in
-p/--no-session-persistenceone-shot mode (matches
the sub-agent path that already exits cleanly), or
- Close stdin / signal the session once the final result is received.
Environment
- Claude Code 2.1.187 (desktop app)
- macOS (Darwin 25.5.0), Apple Silicon
- Scheduled task cadence:
*/20 * * * *
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗