Injected grep wrapper (bundled ugrep) consumes 20+ GB RAM on bounded-repeat regex, hard-freezes machine
Summary
The grep shell function that Claude Code injects into Bash sessions (which re-execs the Claude binary as bundled ugrep via exec -a ugrep "$CLAUDE_CODE_EXECPATH" -G --ignore-files --hidden -I ...) has a catastrophic memory blowup on regex patterns with bounded repeats around an alternation, e.g.:
.{0,100}(word one|word two|prefix [0-9]|word3|word4).{0,100}
With -o -i -E and this pattern shape, memory grows to 20+ GB RSS at 100% CPU within ~2 minutes — on input files totalling only ~600 KB. On my machine (30 GB RAM) this exhausted physical memory, the kernel OOM killer never fired (the system thrashed into zram/swap instead — kswapd0 logged a WARNING ... mm/page_alloc.c:4654 __alloc_pages_slowpath page-allocation failure), and the machine hard-froze and had to be power-cycled. After reboot the resumed session re-ran the same command and it climbed back to 21 GB before I killed it manually.
GNU grep runs the identical pattern on the identical file in 0.88 s with 180 MB peak RSS.
Environment
- Claude Code: 2.1.211 (native install,
~/.local/share/claude/versions/2.1.211) - OS: Linux Mint 22 (Ubuntu 24.04 base), kernel 6.17.0-40-generic, x86_64
- RAM: 30 GB + zram (15 GB) + 8 GB swapfile
- Shell: bash
Minimal reproduction
Generate a ~166 KB synthetic HTML file (no long-line pathology needed — longest line ~21 KB):
python3 -c "
import random
random.seed(42)
words = ['lorem','ipsum','dolor','sit','amet','consectetur','adipiscing','elit','sed','do','eiusmod','tempor']
with open('repro.html','w') as f:
f.write('<html><head><title>test</title></head><body>')
for i in range(400):
line = ' '.join(random.choice(words) for _ in range(60))
f.write(f'<div class=\"c{i}\"><p>{line}</p></div>')
if i % 50 == 0:
f.write('\n')
f.write('</body></html>\n')
"
Run through the injected wrapper (or equivalently call the binary directly as ugrep). Warning: cap memory first or this will take the machine down:
PAT='.{0,100}(lorem ipsum|dolor sit|amet [0-9]|tempor run|eiusmod|elit|sed do|consectetur adipiscing).{0,100}'
( ulimit -v 3000000
/usr/bin/time -v timeout 30 bash -c \
"exec -a ugrep ~/.local/bin/claude -G --ignore-files --hidden -I -o -i -E '$PAT' repro.html" >/dev/null )
Result:
ugrep exit: 139 (SIGSEGV after hitting the 3 GB ulimit)
Elapsed (wall clock) time: 0:15.65
Maximum resident set size (kbytes): 2879192 # ~2.9 GB in 15 s, still climbing
Without the ulimit, observed 21.6 GB RSS in under 2 minutes before manual SIGKILL.
Comparison with GNU grep, same pattern, same file:
$ /usr/bin/time -f "%e sec, %M KB peak" /usr/bin/grep -o -i -E "$PAT" repro.html | wc -l
0.88 sec, 180628 KB peak
912
Why this is nasty in practice
- The wrapper is invisible: the agent (and user) writes plain
grepin a Bash tool call and gets bundled ugrep with different performance characteristics. - Bounded-repeat context windows (
.{0,100}...) are a pattern the model itself commonly generates to extract text around a match. - There is no memory guard on the spawned process, and by the time the desktop is unresponsive it's too late to kill it — the failure mode is a full system freeze with no OOM kill and data loss from the power-cycle.
- The command was re-executed on session resume after reboot, immediately re-triggering the blowup.
Expected behavior
Performance in the same ballpark as GNU grep/ripgrep for the same pattern, or a fast failure ("pattern too complex") — not unbounded memory growth. A ulimit/cgroup memory cap on the wrapper-spawned process, or falling back to system grep for patterns with bounded repeats, would also prevent the freeze.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗