grep shell shim (claude-as-ugrep): unbounded memory growth on ERE bounded quantifiers {m,n} — OOMs the host

Open 💬 2 comments Opened Jul 4, 2026 by d4gold

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's grep() 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗