anthropics / anthropics/claude-code
macOS: background Bash tasks orphaned at PID 1 with their .output files already unlinked — 30 spinning shells held 7.6/10 cores for 25h (2.1.245)
- Ngôn ngữ chính
- Python
- Star
- 145k
- Fork
- 23.1k
- Chỉ số merge pull request
- Chỉ số pull request đang chờ
Mô tả
## Summary
On macOS, **harness-registered background Bash tasks** (`run_in_background: true`) survived
their session and were adopted by launchd at PPID 1. Thirty spinning `zsh` processes held
~7.6 of 10 cores for **25 hours** before I found them by accident.
The distinguishing detail: Claude Code had already **deleted the tasks' `.output` files**
while the processes were still alive and holding write fds on them. Some cleanup path ran and
reaped the bookkeeping without terminating, or even signalling, the processes it belonged to.
- Claude Code **2.1.245**
- macOS 26.6.2 (Darwin 25.6.0), arm64, 10-core Apple Silicon
## Forensics from the live incident
Thirty `/bin/zsh` processes, all `PPID 1`, in three groups of ten by start time
(2026-08-24 14:13:09, 14:15:15, 14:16:48 — matching three background tasks):
```text
PID PPID %CPU ELAPSED COMMAND
92318 1 21.2 01-01:17:38 /bin/zsh
92319 1 21.6 01-01:17:38 /bin/zsh
... (30 total, ~21-28% CPU each)
```
`sample` on one of them — 872 of 872 stack samples in a pure in-shell loop, no syscalls
beyond `sigprocmask` from zsh's own signal blocking. `execstring` means these are `zsh -c`
string bodies, i.e. tool-spawned, not interactive shells:
```text
872 zsh_main + 1272
872 init_misc + 152
872 execstring + 132
872 execode + 188
...
872 execbuiltin + 1628
...
872 execfor + 1072
...
440 execwhile + 464
```
`lsof` — fd 0 is `/dev/null`, fds 1 and 2 point at a task output file, ten processes per file
across exactly three files:
```text
zsh 92318 user cwd DIR /Users//code/
zsh 92318 user 0r CHR /dev/null
zsh 92318 user 1w REG /private/tmp/claude-501/-Users--code-//tasks/.output
zsh 92318 user 2w REG /private/tmp/claude-501/-Users--code-//tasks/.output
```
**Those three `.output` files no longer exist on disk.** `lsof` still resolves the paths, but
`ls` returns `No such file or directory` for all three. They were unlinked while their writers
were running, so nothing could ever have read that output again — including a resumed session.
Aggregate cost while it ran: `top` reported `0.0% idle` with 44 running threads on a 10-core
box; killing exactly these 30 processes dropped the 1-minute load average by ~30 and returned
the machine to ~67% idle.
One observation I cannot fully explain, included in case it points at the responsible path:
the owning session's transcript (`~/.claude/projects//.jsonl`) was last
written 2026-08-23 12:48, over a day *before* these processes started at 2026-08-24 14:13,
even though their fds point into that session id's task directory.
## Why this is worth fixing beyond the wasted CPU
Silent, unbounded CPU consumption is the obvious cost. The second cost is that it corrupts
unrelated work on the same machine.
In my case a repository timing gate reads the 1-minute load average to decide whether
wall-clock measurements are trustworthy. With these 30 always-runnable threads in the mix it
read 4.7-6.9 per core against a 0.5 ceiling, and the investigation that produced those numbers
concluded that *macOS load average is unusable as a signal on a working dev box* — and opened
a ticket to redesign the gate around that false conclusion. Once the orphans were killed, the
same box measured 0.37-0.48 per core and the gate worked exactly as designed. A leak like this
does not just burn cores; it manufactures convincing evidence for wrong engineering decisions.
## Relationship to existing issues
I checked the open orphan cluster and believe this is a distinct trigger:
- **#81462** (macOS, PID 1) is a *foreground* Bash call whose shell-level `&` child escapes.
Mine were registered background tasks with task IDs and output files — the runner knew about
them.
- **#81210** is Linux and crash-triggered, and correctly notes JS-layer exit handlers cannot
help on a hard crash. No crash was observed here, and the partial cleanup (output files
unlinked) is evidence that a cleanup path *did* run.
- **#80885** is scheduled-task sessions leaking `claude` process pairs, not Bash tool tasks.
- **#77593** is Windows.
The overlap suggests one underlying gap: **background task teardown is not tied to task
deletion.** #81210's suggested primitives are the right shape — spawn each task into its own
process group and kill the group, and add a startup sweep that reaps tasks recorded by a
session whose PID is gone. On macOS the parent-death primitive is `kqueue`/`NOTE_EXIT`, since
`PR_SET_PDEATHSIG` is Linux-only.
At minimum: **whatever unlinks a task's `.output` file should first terminate that task's
process group.** In this incident that single ordering change would have prevented all 30
orphans.
## Repro
I did not cause this deliberately and have **not** established a reproduction — the forensics
above are from the live incident, and the processes are now killed. A plausible shape, offered
untested: start a long-running `run_in_background` Bash task that spawns child shells, end or
abandon the session without stopping the task, and check for `PPID 1` survivors plus whether
the task's `.output` file was unlinked underneath them.
Happy to instrument a box and chase a deterministic repro if that would help.
Hướng dẫn đóng góp
Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này
Hướng nghiên cứu
No source file or test is named. Start by attempting the proposed run_in_background reproduction, then trace the task-deletion path that unlinks the .output file and inspect how process groups and kqueue/NOTE_EXIT could be used on macOS. Done means abandoned background tasks are terminated before their output is unlinked and startup cleanup handles tasks whose session PID is gone.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Đánh giá
- Công nghệ
- bash, macos, zsh
- Lĩnh vực
- cli, devtools, operating-systems
- Loại issue
- Lỗi
- Độ khó
- 5/5
- Thời gian dự kiến
- Hơn một tuần
- Mức độ hoạt động
- Sôi nổi
- Độ rõ ràng
- Cần làm rõ
- Mức phù hợp với người mới
- 35/100