[BUG] Bundled ugrep --ignore-files disregards root-anchored .gitignore patterns (/node_modules, /target): searches walk dependency trees and can stall the machine
Summary
The bundled ugrep, invoked with --ignore-files, does not honor root-anchored .gitignore patterns. A pattern of the form /node_modules or /target (leading slash, anchoring the match to the directory containing the .gitignore) is silently disregarded and the search descends into the directory anyway. The unanchored form node_modules/ is honored correctly.
Both forms are valid gitignore syntax and git check-ignore agrees they ignore the same paths, so a repository that uses the anchored form gets no protection from --ignore-files at all. The anchored form is extremely common: it is what npm init, cargo new, and the GitHub Node.gitignore / Rust.gitignore templates emit.
Impact
This is not just a correctness issue, it can take a machine down. A skill-name lookup issued by Claude Code ran as:
ugrep -G --ignore-files --hidden -I --exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg \
--exclude-dir=.bzr --exclude-dir=.jj --exclude-dir=.sl -rl <pattern>
rooted above a directory of checked-out projects. Every one of those projects correctly ignores its heavy directories, but with the anchored syntax, so the scan walked all of them. Observed on a workstation with a spinning /home:
/dev/sdbpinned at 94% utilisation with queue depth ~15 and 46-54 ms read latency- 86% iowait, CPU otherwise 90% idle
- load average 23, with 18-21 processes stuck in uninterruptible (
D) state - one process had read 11.7 GB after 20 minutes and was still going, working through
node_modulesand Rusttarget/debug - a plain
lsin an unrelated terminal took over 30 seconds to return - one project alone contributed 46,383 files that
.gitignoresaid to skip
The two scans had run for 20 and 22 minutes before being killed manually. Killing them returned %util to ~2% and blocked processes to 0 immediately.
The failure is silent in both directions: nothing reports that the ignore rules were disregarded, and the resulting stall presents as a general machine slowdown with no obvious culprit, since the offending processes show comm as the Claude Code version string (2.1.222) rather than ugrep.
Reproduction
#!/usr/bin/env bash
set -u
UGREP_BIN="${1:?usage: repro.sh /path/to/claude/versions/<ver>}"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
for pat in '/node_modules' 'node_modules/'; do
rm -rf "$T/repo"; mkdir -p "$T/repo/node_modules/pkg" "$T/repo/src"
git -C "$T/repo" init -q
echo NEEDLE > "$T/repo/node_modules/pkg/dep.js"
echo NEEDLE > "$T/repo/src/app.js"
printf '%s\n' "$pat" > "$T/repo/.gitignore"
git_says=$(git -C "$T/repo" check-ignore -q node_modules/pkg/dep.js && echo IGNORED || echo "not ignored")
hits=$(bash -c 'exec -a ugrep "$1" -G --ignore-files --hidden -I -rl NEEDLE "$2"' _ \
"$UGREP_BIN" "$T/repo" 2>/dev/null | grep -c node_modules)
printf 'pattern %-16s | git check-ignore: %-11s | ugrep descended into node_modules: %s\n' \
"'$pat'" "$git_says" "$( [ "$hits" -gt 0 ] && echo 'YES (bug)' || echo 'no' )"
done
Run it against the Claude Code binary:
$ ./repro.sh ~/.local/share/claude/versions/2.1.222
pattern '/node_modules' | git check-ignore: IGNORED | ugrep descended into node_modules: YES (bug)
pattern 'node_modules/' | git check-ignore: IGNORED | ugrep descended into node_modules: no
The exec -a ugrep is how Claude Code invokes its own binary for search: the executable is the Claude Code binary itself with argv[0] set to ugrep, which is why ps shows comm as the version string. ugrep is statically embedded, not a separate installed tool.
Behaviour is the same whether the search root is the repository root or a directory above it, and whether or not a real .git directory is present.
Expected
--ignore-files should apply anchored patterns the same way git does, anchoring the match at the directory containing the .gitignore that declared it. git check-ignore is the reference: if it reports a path as ignored, the search should skip it.
Actual
Anchored patterns are disregarded entirely, and the search descends into directories the repository explicitly excluded.
Environment
- Claude Code 2.1.222 (native Linux binary,
~/.local/share/claude/versions/2.1.222) - Bundled ugrep 7.5.0 (version string extracted from the binary)
- Linux 6.12.96+deb13-amd64 (Debian 13), x86_64
Notes
Possibly related, same flag and same bundled engine, but distinct failure modes:
- #83326 -
--ignore-filesskipping files it should search (this report is the inverse: not skipping files it should skip) - #81916 -
--ignore-filesbusy-looping when it resolves to a directory
For anyone hitting the stall before a fix lands, the diagnostic is ps -eo stat,pid,wchan:25,args | awk '$1 ~ /D/', and rewriting /node_modules to node_modules/ in the affected .gitignore files restores the intended behaviour. Note that ~/.ignore, which ripgrep and fd honor, has no effect here, since --ignore-files reads only .gitignore.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗