Scheduled-task CLI process does not exit after unattended run completes
Summary
A scheduled task's underlying claude CLI process does not terminate after the unattended run finishes its turn. Over ~24 hours, this produced 24 separate leaked claude processes on one machine, each still consuming 2-11% CPU hours after their run should have completed - enough concurrent load to push a local Nuxt/Vite dev-server boot from ~1-2 minutes to over 10 minutes (628s Nitro build observed), causing unrelated test-suite timeouts.
Environment
- macOS (Darwin 23.2.0), Claude Code desktop app, claude-code version 2.1.255
- Task type: a CronCreate-style scheduled task (SKILL.md-based), cron expression
23 * * * 1-5(hourly, weekdays) - The task's own script logic is a short synchronous Node script that calls process.exit(0) on its one early-exit path and otherwise falls off the end of the file with no open handles/timers - ruled out as the cause of the hang (see below).
Evidence
ps -eo pid,etime,pcpu,command showed 24 distinct claude CLI processes (command line matching claude --output-format stream-json ... --allowedTools ..., no --resume=<uuid> flag - i.e. each is a fresh, non-resumed session, consistent with a scheduled task rather than an interactive/resumed one), spaced almost exactly 60 minutes apart:
PID ETIME %CPU
16348 23:53:07 11.5
50410 23:27:38 3.6
70106 23:10:21 7.0
43689 22:09:52 6.9
25737 21:10:07 2.6
7879 20:09:55 7.6
89352 19:10:06 6.4
71643 18:10:06 7.6
53612 17:10:06 4.9
35791 16:10:06 7.2
17998 15:10:06 7.3
99521 14:10:06 4.3
81679 13:10:06 3.1
64152 12:10:06 5.2
46082 11:10:06 6.3
28273 10:10:06 2.5
10472 09:10:05 8.7
91930 08:10:05 7.5
73918 07:10:05 7.3
54536 06:10:02 6.4
29659 05:10:00 6.6
6895 04:10:01 7.5
83886 03:10:01 5.2
60839 02:10:01 6.7
36734 01:10:01 6.7
The corresponding session records (via the CCD session-management MCP tools) showed each of these runs as isRunning: false - i.e. the app itself believed the task had completed - yet the OS process was still alive and actively burning CPU at the time of inspection, for as long as ~24 hours after its run should have finished.
Only the most recent firing (10 minutes old at time of inspection) was legitimately still running.
What I ruled out
- Not the task's own script: the scheduled task runs a short Node script via a single synchronous execFileSync call per step; it calls process.exit(0) on its --clear branch and otherwise has no async work, timers, or open sockets that would keep Node's event loop alive.
- Not an interactive/resumed session: none of the 24 stuck processes carried a --resume=<uuid> flag; two other long-running processes on the same machine that did carry --resume= (one of them the conversation used to file this report) behaved correctly and were unaffected.
- Not a stale/wrong-server issue: this reproduced across ~24 independent firings of the same task over a full day.
Impact
With N scheduled tasks configured at similar frequency, leaked processes accumulate without bound (24 in ~24 hours here) since they never get cleaned up on their own. Each is a live Node/Claude process, so this degrades local machine performance for any concurrent work (a local dev-server build/boot went from ~1-2 minutes to 628 seconds under this load), and there's no user-facing signal that this is happening - the session/task UI reports isRunning: false for these while the OS process is still very much alive.
Repro
- Create a scheduled task (SKILL.md-based, via the scheduled-tasks mechanism) that fires frequently (e.g. hourly).
- Let it fire several times unattended.
- After a few hours, check
ps -eo pid,etime,pcpu,command | grep claude- observe multiple long-lived claude CLI processes with no --resume= flag, one per firing, none of them exited, despite the app reporting each run as completed/not-running.
Suggested area to investigate
Whatever code path handles process teardown after an unattended/scheduled-task session reports its final turn and "completes" - it appears the process is not being reliably killed/reaped even though the session record is correctly marked not-running.
---
Reported via Claude Code session assistance.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗