[BUG] Bundled ugrep --ignore-files disregards root-anchored .gitignore patterns (/node_modules, /target): searches walk dependency trees and can stall the machine

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 1 comment · opened Aug 5, 2026

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/sdb pinned 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_modules and Rust target/debug
  • a plain ls in an unrelated terminal took over 30 seconds to return
  • one project alone contributed 46,383 files that .gitignore said 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-files skipping files it should search (this report is the inverse: not skipping files it should skip)
  • #81916 - --ignore-files busy-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.

View original on GitHub ↗

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