[Windows] Monitor / background Bash pipelines orphan tail/grep grandchildren after bash exits (66 found, oldest 167 h)

Status Open
Reported on v2.1.293
Maintainer reply None cached
Activity 1 comment · opened Oct 8, 2026

Summary

On Windows, background Bash pipelines (Monitor tool, run_in_background Bash, and until/watch loops) leave their grandchildren running after the owning bash exits. The bash that ran tail -n 0 -F log | grep --line-buffered PATTERN is gone, but tail.exe and grep.exe keep running with a dead parent PID until reboot, each spinning CPU on a log that nobody writes to any more.

Environment

  • Claude Code 2.1.293, native Windows (not WSL), Git for Windows bash as the Bash tool shell
  • Windows 11 25H2, build 26200.9457
  • Several concurrent sessions (5–8) running Monitor / background watchers over multi-hour jobs

What I found (2026-10-08)

Process scan of everything whose parent PID no longer exists:

| Orphaned process | Count | Age | Notes |
|---|---|---|---|
| tail.exe -n 0 -F <log> | 29 | 4.5 h to 167 h | watched files last written 4.5 h to 167 h earlier |
| grep.exe --line-buffered -E ... | 28 | same | 15–575 s CPU each, busy on the dead tail pipe |
| conhost.exe | 11 | 5 h to 27 h | hosts of the dead pipelines |

All command lines were tail -F ... | grep --line-buffered ... pipelines of the shape the Monitor tool documents. The newest ones were from Monitor calls that had expired at timeout_ms; the oldest (167 h) watched a deploy log from a session that ended a week earlier. Total was 66 processes and ~260 MB, so the cost is CPU and process count rather than RAM, but on build 26200 every leaked process creation also adds to the known Windows kernel token leak (Toke pool tag), so a session's worth of orphans is not free.

Repro (any Windows box):

# in a Claude Code session, Bash tool with run_in_background (or Monitor with a short timeout_ms):
tail -n 0 -F some.log | grep --line-buffered PATTERN
# let it expire / finish the session, then:
powershell -c "$c=Get-CimInstance Win32_Process; $l=@{}; $c|%{$l[[int]$_.ProcessId]=1}; $c | ? { $_.Name -in 'tail.exe','grep.exe' -and -not $l.ContainsKey([int]$_.ParentProcessId) } | select ProcessId,Name,CommandLine"

Expected: when the Monitor expires, the background task is stopped, or the session ends, the whole pipeline is gone.
Actual: only bash.exe is terminated; tail.exe, grep.exe and their conhost.exe survive with a dead parent.

Likely cause

On Windows, killing the bash PID does not kill its process tree. Git for Windows bash does not propagate termination to pipeline members when it is killed from outside, and there is no Job Object around the tool's process tree, so the grandchildren are orphaned rather than reaped.

Suggested fix

Run each Bash tool / Monitor command inside a Windows Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, and close the job handle when the command is stopped, times out, or the session ends. That kills the entire tree regardless of how bash behaves. (Equivalent of killing the process group on POSIX.)

Workaround I am using

A user-level SessionStart hook that kills tail.exe / grep.exe / conhost.exe whose parent PID is gone and that are older than 1 h. It cannot distinguish a Monitor orphan from a deliberately detached watcher, so a proper fix inside the tool would be better.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗