grep shell shim (claude-as-ugrep): unbounded memory growth on ERE bounded quantifiers {m,n} — OOMs the host
Summary
The shell-integration grep shim (the grep() shell function that re-execs the claude binary with ARGV0=ugrep) has unbounded memory growth when the ERE pattern contains bounded quantifiers like {0,60}. It produces no output, never completes, and grows RSS at ~100–120 MB/s until the host OOMs.
This took down a 16 GB production server twice in one morning: three shim-grep processes reached 14 GB+ RSS each, exhausted swap, and drove load average past 12 (widespread D-state, box unresponsive). Worse, the agent session that issued the grep retries it after the process is killed, so the box re-freezes minutes later — the failure loops.
GNU grep runs the identical pattern on the identical file in 1–2 ms. The same claude binary invoked as rg (ripgrep emulation) also handles the pattern instantly — only the ugrep/grep emulation path is affected.
Environment
- Claude Code 2.1.201 (
/home/<user>/.local/bin/claude, Bun v1.4.0 (63bb0ca0d) Linux x64 baseline) - Linux kernel 6.17, glibc 2.36, zsh
- Triggered organically by a Claude session running
grep -oiE '.{0,50}receipt.{0,60}' file.html-style commands via the Bash tool (the shell snapshot'sgrep()function routes these to the built-in engine)
Minimal reproduction
# 1. Any ordinary text file works — 90 KB, 1000 short lines:
seq 1 1000 | sed 's/$/: roadside service call quote with $95 price and filler words to pad the line/' > /tmp/trivial.txt
# 2. Run the claude binary in its ugrep persona (what the grep() shim does):
( exec -a ugrep ~/.local/bin/claude -oE '[^{};]{0,60}\$95[^{};]{0,70}' /tmp/trivial.txt )
# → no output, RSS grows ~120 MB/s indefinitely; kill it before it OOMs your machine
# 3. Control — GNU grep, same pattern, same file:
command grep -coE '[^{};]{0,60}\$95[^{};]{0,70}' /tmp/trivial.txt
# → "9", in ~1 ms
Measured RSS growth of step 2 (identical curve on a real-world 80 KB HTML file):
t=2s RSS=301MB
t=4s RSS=543MB
t=6s RSS=773MB
...linear, reached 14.8 GB in ~2.5 min on the production incident before being killed
Isolation matrix
| Invocation | Pattern | Result |
|---|---|---|
| exec -a ugrep claude -G -oE | [^{};]{0,60}\$95[^{};]{0,70} | ❌ hangs, unbounded RSS |
| exec -a ugrep claude -oE (no -G) | same | ❌ hangs |
| exec -a ugrep claude -G -E (no -o) | same | ❌ hangs |
| exec -a ugrep claude -E (no -G/-o) | same | ❌ hangs |
| exec -a ugrep claude -G -oE | \$95 (no quantifier) | ✅ instant, correct 9 matches |
| exec -a rg claude -o | [^{};]{0,60}\$95[^{};]{0,70} | ✅ instant, correct 9 matches |
| command grep -oE (GNU) | same | ✅ 1 ms, correct 9 matches |
So: the trigger is bounded quantifiers ({m,n}) in the ugrep emulation's ERE engine, independent of -o/-G. The rg emulation in the same binary is unaffected.
Expected behavior
Match (or fail) in milliseconds like GNU grep and the binary's own rg mode — or at minimum, bail out / cap memory rather than consuming all host RAM with no output.
Impact note
Because the shim transparently intercepts plain grep inside Bash tool calls, models naturally generate grep -oE '.{0,50}<term>.{0,60}'-style context-window searches and have no way to know they are invoking the buggy engine. A single such call can freeze the entire host, and the session then retries it after a kill. Workaround we're using: instruct sessions to use command grep / the Grep tool.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗