Bash tool leaks orphaned child processes on Windows timeout (runaway ind / scans accumulate for hours)
Summary
On Windows, when a Bash-tool command exceeds its timeout (or otherwise ends), Claude Code terminates the bash.exe shell but not its descendant processes. Grandchild processes (e.g. find.exe under Git Bash / MSYS2) keep running as orphans. A find / full-filesystem scan therefore keeps crawling the entire disk for hours after the tool call has "ended," and these accumulate across sessions until the machine is heavily loaded.
Environment
- Claude Code: 2.1.206
- Model: Opus 4.8 (1M context)
- OS: Windows 11 Education, 10.0.22631 (build 22631)
- Shell / Bash-tool backend: Git Bash (MSYS2),
git version 2.36.1.windows.1,C:\Program Files\Git\usr\bin\find.exe
Impact
- Found 10 orphaned
find.exeprocesses alive, spawned over ~4 hours across prior sessions (creation timestamps 2026-07-09 20:09 → 2026-07-10 00:10). - Every one was scanning from filesystem root
/. - Each had accumulated 16,000–26,000 CPU-seconds (4.5–7 CPU-hours) and was still consuming ~10–19% of a core apiece when discovered — collectively saturating the machine.
- Parent
bash.exeshells were all already exited (orphanedParentProcessIdpoints to a dead PID), so nothing would ever reap them.
Leaked command lines (verbatim)
find.exe / -iname slint-1.17* -maxdepth 6
find.exe / -type d -iname slint-1.17*
find.exe / -path */registry/src/*/slint-1.1*/src/api/window.rs
find.exe / -type d -iname slint-*-examples -o -type d -path *slint*/examples
find.exe / -iname flickable.rs
find.exe / -path *registry/src*/i-slint-compiler* -maxdepth 10
find.exe / -iname midi_mapping.json
find.exe / -iname *.slint -path *examples*
find.exe / -iname *.rs -path *slint* (x2)
All are unbounded (or barely bounded) recursive scans rooted at /. On Windows/MSYS2 that walks the whole drive, which routinely exceeds the 120s Bash-tool timeout.
Reproduction
- On Windows, have Claude Code run a slow full-disk command via the Bash tool, e.g.
find / -iname somethingthat takes longer than the tool timeout. - Let the tool call time out (or otherwise end).
- Observe: the
bash.exewrapper is gone, butfind.exeis still running.Get-CimInstance Win32_Process -Filter "Name='find.exe'"shows it alive with aParentProcessIdwhose process no longer exists.
Root cause
The Bash tool does not terminate the full descendant process tree on timeout/cancel/exit on Windows — it appears to kill only the immediate bash.exe. MSYS2 grandchildren survive and keep running. There is no Job Object / process-group kill guaranteeing tree teardown.
Suggested fixes
- On Windows, launch Bash-tool commands inside a Windows Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, so closing the job kills the entire tree on timeout/cancel/exit. - Alternatively, on timeout kill the process tree explicitly (
taskkill /T /Fon the shell PID, or enumerate + terminate descendants). - Consider warning when a Bash command roots a recursive scan at
/on Windows.
Note on killing these
For these MSYS2 processes, both taskkill /F /PID ("no instance running") and PowerShell Stop-Process ("cannot find a process") failed, even though the processes were alive and burning CPU. The WMI Win32_Process.Terminate method worked:
Get-CimInstance Win32_Process -Filter "Name='find.exe'" |
ForEach-Object { Invoke-CimMethod -InputObject $_ -MethodName Terminate }Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Still present on 2.1.237 (this was filed against 2.1.206), and I can add three things the report doesn't cover: user cancellation as a trigger, a pipeline backstop that also fails, and a quantified reason the symptom is so hard to attribute.
1. Cancellation, not just timeout
The tool call was cancelled by me interactively, not timed out:
29 minutes later, with the session long closed:
I ended it manually with
Stop-Process -Forceat that point. It showed no sign of stopping on its own — it was still actively walking the tree when I killed it (the three roots total ~127,000 files / ~19 GB). Nothing in Claude Code ever reaped it, and the session that spawned it had been closed for half an hour.2.
| head -Ndoes not save you — and the reason is interestingThe original command was:
That pipeline is supposed to be self-limiting:
headexits after 50 lines, the pipe closes,grepdies onSIGPIPE.It didn't, because both pipeline members were orphaned, not just
grep.headstayed alive holding the read end of the pipe open.grep -rlhad not yet found 50 matching files, soheadsat blocked waiting for more input,grepkept writing to a perfectly live pipe, never receivedSIGPIPE, and kept walking the tree.So partial process-tree cleanup is worse than none here: killing only
bash.exeleaves a self-sustaining pair. Any bounded-output idiom (| head,| head -n,| sed q) silently loses its bound under this bug.3. Why it's hard to attribute: the antivirus amplification
The orphan's own CPU is small, which is why nothing looks wrong in Task Manager. Microsoft Defender Performance Analyzer (
New-MpPerformanceRecording/Get-MpPerformanceReport), 90-second window while the orphan ran:| Metric | Value |
|---|---|
| Total real-time scans | 7,314 |
| Caused by the orphaned
grep.exe| 7,017 (96%) || Total scan duration | 81,523 ms |
| Caused by the orphaned
grep.exe| 80,267 ms (98.5%) |MsMpEng.exeheld a steady 89–118% of one core across 12 consecutive samples, never dropping below 88.8%. Once the orphan was killed it fell to 2–17%.The orphan itself used only 3–14% of a core — but by opening ~78 files/sec it forced Defender to spend ~11.4 ms per file (up to 1,369 ms on a large DLL). A ~10x–100x amplification. The visible symptom is a pegged core belonging to
MsMpEng.exeand a laptop fan at full speed, with no running task and no open session to explain it. Correlating Defender's CPU against other processes' file I/O shows no relationship (r = 0.12–0.20), because the driver is a process nobody is looking at.Environment
entrypoint: claude-desktop)permissionMode: autoDetection one-liner for anyone else hitting this:
Agreed on the suggested fix — a Job Object with
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEwould close all three triggers (timeout, cancellation, session teardown) at once.Still present on 2.1.272 (filed against 2.1.206, confirmed on 2.1.237 above). Four things to add, the second of which may be the most useful: the PowerShell tool on the same machine does not have this bug, so a working implementation already exists in-product.
1. Controlled A/B: Bash tool leaks, PowerShell tool does not
Same grandchild binary, same machine, same session, same 10s timeout. Only the tool differs.
The Bash tool reported
Exit code 143 / Command timed out after 10s. The PowerShell tool moved its command to a background task, which I then ended via the harness's own stop.State afterwards:
After stopping the PowerShell task, +8s:
So the Bash tool believes it terminated the command (exit 143) while the grandchild keeps running, and the PowerShell tool tears the tree down correctly. Whatever the PowerShell path does on teardown is the behaviour the Bash path needs.
2. The detection one-liner in the comment above over-flags, and the false positives are live work
The published detector keys on a dead parent alone:
On this machine that flagged four processes that were not orphans. All four were in-flight children of tool calls that were still running, age 0.00h. The Bash-tool wrapper exits before its own child does, so every live Bash tool call looks orphaned for its entire duration.
That matters for anyone building a reaper on top of this: a reaper keyed on dead-parent alone kills the session's own running work. The predicate needs age as well, and ideally sustained core fraction. Measured across 48
claude.exeon this box: median core fraction 0.000, max 0.055, while every confirmed orphan sat at 0.74-0.98. The band between 0.06 and 0.74 was empty, so the split is clean rather than a threshold picked by hand.A narrower discriminator that held perfectly here: binary path. All 9 orphans were under
C:\Program Files\Git\. Zeropwsh.exeorphans, consistent with section 1.3.
Stop-Processworked here, and verification timing is a trapThe original report notes
taskkill /F /PIDandStop-Processboth failing, with onlyWin32_Process.Terminateworking. On Git for Windows 2.55.0.windows.5 / Claude Code 2.1.272,Stop-Process -Forcekilled all 7 orphans successfully. Possibly version-dependent, so worth trying before reaching for WMI.The trap is how long teardown takes. Checking too early reads as a failed kill:
Both instruments agreed it was alive at +10s, against a control PID that read correctly. A
findwalking a filesystem does not die instantly. Use accrued CPU over the interval, not liveness, as the discriminator -- a process mid-teardown accrues nothing, while a surviving one keeps burning.4. Unbounded memory is a second failure mode, distinct from CPU burn
Not every orphan is a CPU story. This one:
reached 3.45 GB RSS, growing at a measured 1.1 MB/s, on a machine with 3.0 GB free of 31.7 GB.
grep -owith a wide match window buffers each match, so an orphaned one climbs until it exhausts memory. Killing it returned free RAM from 3.0 GB to 4.6 GB immediately.Rate of accumulation under concurrent sessions
This machine runs many concurrent Claude Code sessions. Seven orphans of this exact shape were cleared about an hour before my sweep -- 126.2 CPU-hours combined, measured by another session on the same box. Four more had appeared within the following 45 minutes, three of them
find /variants. Survivors from earlier were 37, 51 and 51 hours old.So on a multi-session setup this does not reach a steady state. It accumulates roughly as fast as sessions issue searches, and nothing in the product ever reaps them.
Environment
C:\Program Files\Git\usr\bin\andC:\Program Files\Git\mingw64\bin\Agreed on the Job Object fix. Given section 1, it may be worth checking what the PowerShell tool path already does on teardown and applying the same thing to the Bash path.
Hit this again today and isolated the root cause. It is narrower than "find / is slow", and that makes it fixable.
The handle pump is not
/. It is three registry mounts under/proc.Git Bash
/isC:\Program Files\Git, and the MSYS2 runtime mounts the entire Windows registry there as filesystem trees:/proc/registry,/proc/registry32,/proc/registry64. Walking them opens a kernel handle per registry key, andHKEY_CLASSES_ROOTalone is effectively unbounded.Paired arms, same
find.exe, handle count sampled every 3 seconds:| command | handles at 3/6/9/12/15s | output lines | exits? |
|---|---|---|---|
|
find /proc/registry| 7,619 -> 15,590 -> 22,185 -> 26,594 -> 30,471 | 2,202 | no ||
find /proc| 8,643 -> 16,082 -> 23,448 -> 32,342 -> 39,782 | 3,300 | no ||
find /proc -maxdepth 2| n/a | 302 | yes, immediately ||
find /procwith all 3 registry mounts pruned | flat | 3,866 | yes, immediately ||
find /(first 15s) | 205 -> 206 -> 205 -> 206 -> 206 | 285,839 | still in the real filesystem |Two things worth pulling out of that table.
find /looks healthy for a long time. It stays flat at ~205 handles while it walks the real filesystem, because/procsorts aftermingw64andusr. It only becomes the runaway once it descends into/proc. So a short reproduction attempt can easily miss it.Pruning
/proc/registryalone does not work. I tried exactly that and the scan fell straight into/proc/registry32and kept leaking, at almost the same rate. It has to be the whole of/proc, or all three mounts named explicitly.This also explains a detail from an earlier run that had not made sense: a runaway
find.exeat 95 percent of one core showed 4 total disk read operations. It was not reading the disk at all. The registry is in memory.Workaround, verified:
Suggested fix on the tool side: the Bash tool could prune
/procby default for MSYSfind, or the Windows setup notes could say that/is not the filesystem root here and that/procmust be excluded. Either would have prevented every instance of this I have seen.Separately, and this is the half that turns a slow command into a machine-wide outage: a descendant of the Bash tool's
bash.exeis not reaped when the wrapper dies. I measured that directly and wrote it up on #90672 rather than repeat it here. Without that second defect these scans would end with the session instead of running for hours.For scale, on a 20-core / 32 GB Windows 11 box: two orphaned
findand three orphanedgrepreached 4,183,509 machine-wide handles with paged pool at 4,620 MB. A test suite that normally finishes in 10 seconds took 26 minutes. Killing the five orphans brought handles to 255,190.