Background Bash task: grep→ugrep shim fell back to full multithreaded filesystem scan instead of reading piped stdin, making machine unresponsive

Open 💬 0 comments Opened Jun 11, 2026 by leafo

Environment: Claude Code 2.1.173, Linux (Arch), bash.

Claude Code injects a grep() shell function into Bash tool shells that re-execs the claude binary as its embedded ugrep with -G --ignore-files --hidden -I --exclude-dir=.git ....

In one Bash tool call with run_in_background: true, a pipeline of this shape (producer emitted ~11 KB):

{ sleep 2; head -c 11000 /dev/zero | tr '\0' x; } | grep -oE "<li>.{0,300}[Jj]ams.{0,200}" | head -3

resulted in the ugrep process ignoring the pipe and recursively scanning the filesystem from the working directory for 10+ minutes until manually killed. Because ugrep parallelizes across all cores by default and the pattern is a backtracking-heavy bounded wildcard (-o with .{0,300}…{0,200}), the scan saturated every core and enough memory to make the machine unresponsive — the working directory contained multi-hundred-MB text files.

The fallback is intermittent — a race in how background-task stdin is wired, not deterministic: the identical pipeline in foreground, and repeated identical background runs, read stdin correctly. A reliable way to detect the bad path when it occurs: run the pipeline in a directory containing a bait file that matches the pattern while stdin contains no match — any output means grep read the filesystem instead of the pipe.

Two compounding problems worth fixing separately:

  1. The shim should never silently fall back to recursive filesystem search when invoked in a pipeline — if stdin wiring is ambiguous, erroring out is far safer than an unbounded scan.
  2. The fallback amplifies arbitrary model-written regexes into an all-cores, unbounded workload (--hidden, no thread or file-size limits), so a single stray grep can take down the host. Bounding threads and file sizes in the shim would limit the blast radius.

Expected: a grep shim receiving piped input only ever consumes its stdin.

View original on GitHub ↗