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.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Seeing the same thing on 2.1.201, kernel 6.17, 32GB Linux box.
Came up during normal use. A session ran a bog-standard scrape through the Bash tool (curl a docs page, strip nulls, pull text around some keywords):
curl -sL "https://example.com/some/docs/page" \
| tr -d '\0' \
| grep -oiE '[^<>]{0,200}(alpha|bravo|charlie|delta|echo|foxtrot)[^<>]{0,200}' \
| head -40
The
grep()shim turned that into the claude binary re-exec'd as ugrep:exec -a ugrep ~/.local/share/claude/versions/2.1.201 \
-G --ignore-files --hidden -I --exclude-dir=.git ... \
-oiE '[^<>]{0,200}(alpha|bravo|...)[^<>]{0,200}'
What I saw:
GPU process launch failed: error_code=1002, thenGPU process isn't usable. Goodbye.).Couple of things that might help narrow it down:
[^<>]{0,200}twice, not.{0,N}. So whatever #67021 is seeing with two bounded intervals isn't specific to.; a bounded[^...]{0,N}pair does it too.Cross-linking from #77230 (closed as a duplicate of this) — I ended up isolating an even more minimal repro that pins down the exact trigger, might be useful for whoever's fixing this.
Minimal case: single ~120-byte line, no file needed, reproducible with the standalone published
ugrep7.8.2 binary (independent of Claude Code's bundling):Things I ruled out (hang persists regardless):
(...|...)) reproduces it just as reliably.-o— dropping it (plain-Einstead of-oE) still hangs.-Gvs-E(BRE vs ERE) — both hang identically.What it does depend on: a bounded-repetition wildcard on both sides of the match simultaneously, with the bound around ~40. Same one-line input:
.{0,20}...{0,20}completes in ~50ms,.{0,40}...{0,40}never completes (still running after 5+ seconds in every trial; in the original in-the-wild case that prompted #77230 it ran 20+ minutes before being manually killed). Didn't bisect the exact transition point, but 20 vs 40 alone is already a >100x slowdown — far more than the ~2-4x you'd expect from doubling scanned context, consistent with an exhaustive both-sided search (e.g. for POSIX leftmost-longest matching) rather than an early-exit/anchored one.Since this reproduces with the standalone published
ugrepbinary, it may be worth relaying to https://github.com/Genivia/ugrep upstream too, if not already tracked there.Separately (may be worth a distinct issue if not already covered): when the wrapping background shell running the stuck search was sent
SIGTERM, the shell exited but itsugrepchild was not terminated with it — it was reparented to PID 1 and kept consuming ~90-100% CPU until manuallySIGKILL'd. So killing the wrapping shell/task doesn't actually free the CPU in this case.Confirmed — reproduced on 2.1.233 (Linux and macOS): a pattern with a two-digit bounded repeat like
.{0,50}receipt.{0,60}run through the shell'sgrepin a Claude Code session spins with rapidly growing memory and never finishes, while systemgrepandrghandle it instantly.The cause is on our side (the grep compatibility shim Claude Code installs into the tool shell), and a fix is landing in an upcoming release: those patterns are routed to the system
grepinstead. Until you're on a build with the fix,command grep …orrgsidesteps the shim. Thanks for the very precise report — it made this quick to pin down.🤖 Generated with Claude Code