Bundled ugrep 7.5.0 busy-loops at 100% CPU forever when --ignore-files resolves to a directory (e.g. a dir named .gitignore); orphaned children saturate the machine
Summary
The grep shell function injected into the Bash tool by the shell snapshot routes every grep call to the bundled ugrep 7.5.0 with --ignore-files --hidden. If any directory in the search tree contains an entry named .gitignore that is itself a directory, ugrep opens it as an ignore file and enters an unkillable-by-normal-means busy loop: 100% CPU, forever, never returning and never producing output.
The process outlives the tool call. With several concurrent Claude Code sessions these accumulate one per grep until every core on the machine is saturated. On my machine 11 of them were pinned simultaneously; the oldest had been spinning for 50 minutes. Unrelated work degrades badly — git rev-parse HEAD went 0.26s → 0.46s and interactive zsh startup 2.3s → 3.4s, with load average at 22.
A directory named .gitignore is perfectly legal, and upstream ugrep 7.8.2 handles it correctly. This is an old-bundled-version bug.
Minimal reproduction
mkdir -p /tmp/ugrepbug/sub/.gitignore
echo needle > /tmp/ugrepbug/a.txt
cd /tmp/ugrepbug
grep -rn needle .
Run that inside Claude Code's Bash tool. Expected: a.txt:1:needle. Actual: hangs forever at 100% CPU; the tool call times out and the ugrep child keeps spinning after it.
The same thing directly, bypassing the shell function:
ARGV0=ugrep ~/.local/share/claude/versions/2.1.220 \
-G --ignore-files --hidden -I -rn needle /tmp/ugrepbug
Isolation
Same fixture, same flags, three binaries:
| Binary | Flags | Result |
|---|---|---|
| bundled ugrep 7.5.0 | --ignore-files --hidden | hangs, ~99% CPU, indefinitely |
| bundled ugrep 7.5.0 | without --ignore-files | finishes correctly |
| upstream ugrep 7.8.2 (Homebrew) | identical flags | finishes correctly |
So the trigger is specifically --ignore-files resolving its target to a directory, and it is already fixed somewhere between 7.5.0 and 7.8.2.
Evidence
sample on a live spinning process — 2474 of 2575 stacks parked in __select, the rest in fread, i.e. a poll loop on a descriptor that never becomes ready and never reaches EOF:
2575 Thread_... DispatchQueue_1: com.apple.main-thread
...
2475 ??? + 0x381e0
! 2474 ??? + 0x39370
! : 2474 __select (in libsystem_kernel.dylib) + 8
100 ??? + 0x381cc
100 fread → __fread → __srefill1 → _sread → __sread → __read_nocancel
lsof on the same process names the culprit directly — fd 8 is the ignore file, opened as a DIR:
FD TYPE NAME
0r CHR /dev/null
3r DIR <repo>/.claude/fragments
8r DIR <repo>/.claude/fragments/.gitignore <-- opened as an ignore file, is a directory
Why this is easy to hit
Any tool that stores per-target configuration as a directory named after the target file produces this layout. In my case a config-fragment manager keeps .claude/fragments/<target-file>/…, so .claude/fragments/.gitignore/ is a directory. That pattern existed in 38 places across 18 repos plus every ~/.claude/plugins/marketplaces/* clone.
Two shim details make it worse:
--hiddenmeans the walk descends into dotted directories like.claude/, so these are reachable in a normal project-scoped search.--ignore-filesis unconditional, so there is no way to opt a call out of the code path that hangs.
Every agent grep touching such a tree permanently loses a core, and the population only grows.
Suggested fixes
- Bump the bundled ugrep to ≥ 7.8.2, which does not have this bug.
- Defensively, have ugrep
stat()an ignore-file candidate and skip it unless it is a regular file. - Reap shim children when the Bash tool call ends — these survived their parent indefinitely (related: #80230, #79902).
- Provide the opt-out for the
find→bfs /grep→ugrep shadow functions requested in #69736. With no env toggle, there is currently no supported way to avoid this once you hit it.
Environment
- Claude Code 2.1.220 (native build, bundled ugrep 7.5.0)
- macOS 26.5.2, arm64
- zsh 5.9 (
/opt/homebrew/bin/zsh) - Shim source:
~/.claude/shell-snapshots/snapshot-zsh-*.sh, section# Shadow find/grep with embedded bfs/ugrep
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗