anthropics / anthropics/claude-code
Scheduled-task runs leak a resident claude process per fire (2.1.255, Linux): 106 procs / 26 GB in 25 h — regression of #54626
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
**Environment**
- Claude Code: **2.1.255** (bundled in Claude Desktop, Linux)
- OS: Arch Linux (Omarchy), kernel 7.1.9, Wayland/Hyprland
- RAM: 30 GiB + 30 GiB zram + 30 GiB disk swapfile
- Entrypoint: `claude-desktop`, built-in scheduled tasks (~17 registered, cadences from 30 min to daily)
## Summary
Every scheduled-task fire spawns a resident `claude` CLI process that **never exits after the run completes**. Over 25.5 h of uptime this accumulated **106 leaked processes holding ~11.4 GB**, and with per-session MCP servers layered on top the app's total footprint reached **~26 GB of 30 GB**, saturating zram and pushing ~20 GB onto the disk swapfile.
This is the same failure described in **#54626**, which was closed as COMPLETED on 2026-06-16 at ~2.1.119–2.1.149. It reproduces on **2.1.255**, on Linux, at roughly 70× the reported magnitude (that report cited ~1.5 GB over a week; this is ~11 GB in a day). Filing fresh rather than commenting on a long-closed ticket, but this should probably be treated as a regression of it. Related but distinct: #83218 (MCP subprocesses surviving app quit, macOS), #85986 (VSCode extension spawning a process per discussion).
## Evidence that it's the scheduled tasks, not user-opened chats
Three independent lines, all from one host:
1. **Process census.** 144 resident `claude` processes under a single desktop app instance (uptime 25.4 h). Classifying each by the first user message in its transcript: **106 were scheduled-task runs, 38 were real interactive chats.**
2. **Start times are cron-shaped, not human-shaped.** Session start minute-of-hour clusters hard — `:04` ×17, `:05` ×17, `:34` ×15, `:49` ×14, `:19` ×12 — across only 36 distinct minutes of 60. Sessions also appear steadily overnight while the machine is unattended (00:13, 01:18, 02:05, 05:07, 06:01, 06:36).
3. **The transcripts say so directly.** Every one of those overnight sessions opens with ``, and each corresponds to a registered task's configured fire time.
Leak concentrated in the high-frequency tasks, exactly proportional to fire count:
| task (cadence) | leaked resident procs |
|---|---|
| poller-a (~30 min) | 50 |
| poller-b (~90 min) | 23 |
| mail-sync (hourly) | 13 |
| 14 others (daily) | 20 |
Measured rate: **~4.2 leaked processes/hour**, ~110 MB each.
Session registry corroborates: `~/.claude/sessions/` held **143 entries, only 1 with a dead PID** — i.e. essentially every task run since app start was still resident. All carried `"kind":"interactive"`, `"entrypoint":"claude-desktop"`, `"nameSource":"derived"`.
## Compounding factor: one MCP server per session, spawned whether used or not
Each leaked session also holds its full MCP server set. With the Playwright plugin enabled globally, that was **284 extra processes (1 npm wrapper + 1 node server per session) totalling 10.9 GB** — for a tool the vast majority of those task runs never call. Disabling that one plugin cut per-session cost from ~185 MB to ~110 MB.
Two things would help independently of the leak fix:
- don't spawn a plugin's MCP server until the session actually resolves one of its tools
- ensure a session's MCP children die with it
A third, smaller contributor: `/tmp/claude-/` scratchpad directories are not cleaned up per session and are RAM-backed on tmpfs (306 dirs / 3.7 GB here).
## Impact
`free -h` at the point the desktop became unusable:
```
total used free available
Mem: 30Gi 29Gi 612Mi 818Mi
Swap: 60Gi 50Gi
/swap/swapfile 30.4G 19.9G used
/dev/zram0 30.4G 30.3G used (100% full)
```
Memory PSI `full avg300 = 1.35`, IO PSI `full avg300 = 3.69`, **CPU PSI `full = 0.00`**. That last point is a diagnostic trap: the machine presents as generally sluggish with no CPU load at all, so anything that looks at CPU first finds nothing.
## Reproduction
1. Claude Desktop on Linux, 2.1.255, with several scheduled tasks registered (at least one at 30-min cadence).
2. Leave the app running ~24 h.
3. `ps -eo pid,rss,args -C claude | grep claude-code | wc -l` — expect roughly one process per task fire since app start, none of them exiting.
4. Confirm classification: for each PID in `~/.claude/sessions/*.json`, read the first `type:"user"` record of `~/.claude/projects/*/.jsonl`; leaked ones begin with `/cmdline` before killing to guard against PID reuse. Validated by replaying 919 transcripts: 145 of 147 leaked runs correctly identified, 2 genuinely-interrupted runs spared, all 772 interactive chats untouched. Runs on a 4-hour user timer.
**One detail that may be useful to whoever fixes this:** transcript `.jsonl` files do not end with the final conversational record. They end with bookkeeping entries — `last-prompt`, `bridge-session`, `atis-latch`, `queue-operation`. A naive "is the last record a completed assistant turn?" check judged only 21 of 147 finished runs complete; skipping non-conversational types and reading the last `user`/`assistant`/`result` record raised that to 145. If any internal lifecycle logic decides "is this run done?" by inspecting the tail of the transcript, that may be the bug.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Reproduce the scheduled-task run with claude-desktop, then inspect ~/.claude/sessions/*.json and the corresponding ~/.claude/projects/*/.jsonl transcripts, focusing on lifecycle records after the final conversational turn. Confirm that completed scheduled-task processes and their child processes exit after a run, without affecting interrupted runs or interactive chats.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- linux, python
- Domaine
- cli, devtools, operating-systems, performance
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Active
- Clarté
- Plutôt claire
- Accessibilité débutants
- 45/100