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

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Jul 28, 2026

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:

  • --hidden means the walk descends into dotted directories like .claude/, so these are reachable in a normal project-scoped search.
  • --ignore-files is 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

  1. Bump the bundled ugrep to ≥ 7.8.2, which does not have this bug.
  2. Defensively, have ugrep stat() an ignore-file candidate and skip it unless it is a regular file.
  3. Reap shim children when the Bash tool call ends — these survived their parent indefinitely (related: #80230, #79902).
  4. 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

View original on GitHub ↗

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