Scheduled-task sessions never terminate — leaks ~48 headless processes/day, GBs of RAM
Environment: Claude Code 2.1.197 bundled with Claude Desktop (entrypoint: claude-desktop), macOS 26 (Darwin 25.2.0), Apple Silicon (M-series), 16 GB RAM.
Summary: Headless sessions spawned by the Claude Desktop scheduled-task runner never exit after completing their task. Each firing of a scheduled task leaves a permanently hung claude --output-format stream-json process and a stale entry in ~/.claude/sessions/.
Evidence (measured before cleanup):
- A 30-minute cron task (defined in
~/.claude/scheduled-tasks/<name>/SKILL.md) leaked 105 processes over ~2.5 days — one per firing, uptimes at exact 30-minute multiples, session-file mtimes on :01/:31 marks. - Combined RSS ~8.5 GB (8482 MB → 717 MB after killing them; ~7.8 GB freed).
- All 105 ignored SIGTERM and required SIGKILL — hard-hung, not draining.
- Each leaves
~/.claude/sessions/<pid>.json("kind":"interactive","entrypoint":"claude-desktop","nameSource":"derived"), so the session sidebar in both Claude Desktop and the Claude Code webview fills with dozens of dead entries. - Side effect: memory pressure destabilized concurrent interactive sessions on the same machine.
Steps to reproduce:
- In Claude Desktop, create a scheduled task on a 30-minute cadence (ends up under
~/.claude/scheduled-tasks/). - Let it run for a few hours.
- Compare
ps aux | grep 'output-format stream-json'andls ~/.claude/sessions/— one hung process + one session file per firing.
Expected behavior: The headless session exits when its turn completes, and its session-registry file is removed (or at least excluded from the session sidebar).
Workaround used: built a kill list from the session JSONs filtered by the scheduled task's cwd and process age > 45 min (to protect interactive windows), SIGTERM → SIGKILL each pid + its wrapper parent, then removed the session files of confirmed-dead pids.