[BUG] Windows/Git Bash: Bash tool does not reap its child on timeout - orphans spin at 100% CPU and accumulate across sessions
Environment
- Claude Code on Windows 11 (26200), Git Bash (
C:\Program Files\Git\usr\bin) - i5-14600K, 20 logical cores
Summary
When a Bash tool call times out, the tool call ends but the spawned child process is
not reaped. The child keeps running, is orphaned when its parent bash exits, and
survives the session that created it. Successive calls orphan more children, which
accumulate across sessions with no upper bound.
This is general to any long-running child (npm, pytest, a dev server). It is merely
most visible with Git Bash find, which spins instead of exiting.
This is not the subagent-orphan issue
Flagging up front, because the closest existing reports are a different defect and the
duplicate bot will likely try to file this with them:
- #19045, #24554, #25180 are all about Task-tool subagent processes (
claude --resume
Node processes) surviving session end, on Linux. All closed (not planned / duplicate),
and locked.
This report is a different layer: the Bash tool's own shell child, orphaned at
tool-call timeout (not session end), on Windows/Git Bash. Different process,
different trigger, different platform. #19045 being closed does not cover it.
Observed
Over ~90 minutes, sessions doing KiCad footprint lookups accumulated 20 orphanedfind.exe processes. Killed. A second wave of 4 appeared over the next 15 minutes
and ran for ~4.5 hours undetected:
| Fact | Value |
|---|---|
| Orphaned find processes (2nd wave) | 4 |
| CPU per process | ~98% of one core, continuous |
| Cumulative CPU burned (2nd wave) | ~64,000 sec (~17.8 core-hours) |
| Bytes read per process | 538 |
| Read operations per process | 6 |
| Parent process | exited; all orphaned |
The machine sat at 82-100% CPU. Killing the four returned it to 8-19%.
The tell: 538 bytes read
After 16,000 CPU-seconds, each process had performed 6 read operations totalling
538 bytes. They were never traversing the filesystem. find / -iname <name> under
MSYS enters a userspace spin and never terminates, so it never hits any natural
completion that would let the process exit.
I could not fully isolate the spin's mechanism. A dead-pipe/SIGPIPE theory (find |, head exits first) fits 3 of the 4, but the 4th had no pipe partner and spun
head
identically. Reproduces on demand: spawning find / -iname <nonexistent> directly
still runs after 4 seconds, where the small MSYS root should complete in well under
a second.
Why it compounds
Two behaviours multiply:
- MSYS
findnever terminates on its own. - The Bash tool does not kill the process tree when the call times out.
Either alone is survivable. Together, every timed-out call leaks a core permanently,
and nothing surfaces it. The user's only signal is the machine getting slow hours later.
Reproduce
- In Claude Code on Windows, issue a Bash call:
find / -iname "some-nonexistent-file" - Wait for the tool call to time out (~2 min).
Get-Process find- the child is still running, still at ~98% CPU, parent gone.- It outlives the session.
Suggested fix
Kill the child process tree when a Bash tool call times out or is otherwise
terminated, rather than only ending the tool call. On Windows, a job object would
make this reliable regardless of what the child does with signals.
Workaround in use
A PreToolUse hook refusing root-rooted find, plus a SessionStart hook that
reaps orphans. This is a local guard for one instance of the general problem; it
does nothing for an orphaned npm or pytest.
---
Secondary note (separate, lower priority)
The update-config skill's hook examples all pipe hook stdin through jq
(jq -r '.tool_input.command'). jq is not present on this machine and is not a
Windows default. A hook built by copying those examples fails silently, because the
documented pattern ends in 2>/dev/null || true, which swallows thejq: command not found. Worth either bundling jq, detecting its absence, or giving
a non-jq example for Windows.