Summary
The grep shim installed by Claude Code's shell snapshot routes grep to the
embedded ugrep inside the claude binary. On a regex of the form
.{0,N}(alt1|alt2|...).{0,M} — a bounded quantifier on both sides of an
alternation — the embedded ugrep allocates ~29 GB of RSS during regex
compilation, before reading any input, and is killed by the kernel OOM killer.
Input size is irrelevant: a 28 KB file triggers it. The allocation is
deterministic — total-vm matched to the kilobyte across three separate kills.
On a shared 31 GB host this took the whole machine down: everything else was
pushed to swap and sshd stopped completing handshakes for ~40 minutes. It
happened three times before we identified the cause, because the failure looks
like "a slow regex" from inside the session, not like a memory bug.
Environment
- Claude Code
2.1.220, native binary
- Linux
6.8.0-136-generic, 6 cores, 31 GB RAM, 8 GB swap
- System
grep (GNU grep) 3.11 — unaffected, the problem is only inside
Claude Code Bash calls
Reproduction
One line, on any text file of a few KB:
# inside a Claude Code Bash tool call (so the shim is active)
grep -oiE ".{0,45}(RCS|SAS|SIRET|Paris|75[0-9]{3}).{0,35}" somefile.txt
Expected: a handful of matches.
Actual: RSS climbs to ~29 GB in seconds, then OOM-kill.
The file we hit it on was 28 445 bytes, 264 lines, longest line 4 892 chars.
Safe comparison on the exact same file and pattern:
/usr/bin/grep -oiE '<pattern>' → 13 matches in 0.104 s
embedded ugrep, ulimit -v 2 GB → SIGSEGV, 0 lines
embedded ugrep, no limit → 29 GB RSS → OOM-kill
Bisect
Each run under ulimit -v 2000000 so it segfaults instead of taking the host
down:
| pattern |
flags |
result |
.{0,45}(RCS|SAS|SIRET|Paris|75[0-9]{3}).{0,35} |
-oiE |
SEGV |
same, without -i |
-oE |
SEGV |
same, without -o |
-iE |
SEGV |
without the trailing .{0,35} |
-oiE |
ok, 18 lines |
without the leading .{0,45} |
-oiE |
ok, 13 lines |
bare alternation (RCS|SAS|SIRET|Paris|75[0-9]{3}) |
-oiE |
ok, 20 lines |
.{0,45}(SIRET).{0,35} — one branch, both bounds |
-oiE |
ok |
So the trigger is specifically bounded quantifier on both sides × alternation
with more than one branch. -i and -o are not involved.
Kernel evidence
Jul 29 07:40:18 Killed process 2527415 anon-rss: 29 139 584 kB total-vm: 33 841 168 kB
Jul 29 08:24:31 Killed process 2539464 anon-rss: 28 779 264 kB total-vm: 33 841 232 kB
Jul 29 09:21:23 Killed process 2556134 anon-rss: 28 858 240 kB total-vm: 33 841 232 kB
Identical total-vm across three runs on different files supports the reading
that this is compile-time state expansion, not data-dependent matching.
Host impact each time: load average to 304 on 6 cores, swap exhausted
(7.6 of 8.0 GB), PSI memory full 65 % and io full 53 % over 5 minutes,
co-tenant services (postgres, n8n, others) evicted to swap, sshd accepting TCP
on :22 but dropping the session before the handshake completed.
The shim
~/.claude/shell-snapshots/snapshot-bash-*.sh:
# Shadow find/grep with embedded bfs/ugrep
unalias grep 2>/dev/null || true
function grep {
...
(exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@")
}
Why this is worth fixing rather than documenting
- The pattern shape is completely ordinary — "show me N characters of context
around any of these words" is a normal thing to write, and it is what a model
naturally writes when asked to check whether a fact is present in a file.
- The failure is silent and misleading from inside the session: the Bash call
just exceeds its timeout and gets backgrounded. Nothing indicates memory.
That is why we re-ran it twice more.
grep is shadowed transparently, so neither the user nor the model has any
signal that a different engine with different characteristics is in play.
- Blast radius is the whole host, not the session. On shared or CI machines one
agent can evict everything else.
Suggested mitigations, in order of usefulness
- Cap the embedded ugrep's regex-compilation memory and fail with an error
instead of allocating without bound.
- Impose an address-space limit (
RLIMIT_AS) on the shim's subprocess, so the
worst case is one dead process rather than a dead machine.
- Fall back to system
grep when compilation exceeds a budget.
- At minimum: make the shim visible, so
grep --version inside a Claude Code
Bash call does not look like GNU grep.
Summary
The
grepshim installed by Claude Code's shell snapshot routesgrepto theembedded ugrep inside the
claudebinary. On a regex of the form.{0,N}(alt1|alt2|...).{0,M}— a bounded quantifier on both sides of analternation — the embedded ugrep allocates ~29 GB of RSS during regex
compilation, before reading any input, and is killed by the kernel OOM killer.
Input size is irrelevant: a 28 KB file triggers it. The allocation is
deterministic —
total-vmmatched to the kilobyte across three separate kills.On a shared 31 GB host this took the whole machine down: everything else was
pushed to swap and sshd stopped completing handshakes for ~40 minutes. It
happened three times before we identified the cause, because the failure looks
like "a slow regex" from inside the session, not like a memory bug.
Environment
2.1.220, native binary6.8.0-136-generic, 6 cores, 31 GB RAM, 8 GB swapgrep (GNU grep) 3.11— unaffected, the problem is only insideClaude Code Bash calls
Reproduction
One line, on any text file of a few KB:
Expected: a handful of matches.
Actual: RSS climbs to ~29 GB in seconds, then OOM-kill.
The file we hit it on was 28 445 bytes, 264 lines, longest line 4 892 chars.
Safe comparison on the exact same file and pattern:
Bisect
Each run under
ulimit -v 2000000so it segfaults instead of taking the hostdown:
.{0,45}(RCS|SAS|SIRET|Paris|75[0-9]{3}).{0,35}-oiE-i-oE-o-iE.{0,35}-oiE.{0,45}-oiE(RCS|SAS|SIRET|Paris|75[0-9]{3})-oiE.{0,45}(SIRET).{0,35}— one branch, both bounds-oiESo the trigger is specifically bounded quantifier on both sides × alternation
with more than one branch.
-iand-oare not involved.Kernel evidence
Identical
total-vmacross three runs on different files supports the readingthat this is compile-time state expansion, not data-dependent matching.
Host impact each time: load average to 304 on 6 cores, swap exhausted
(7.6 of 8.0 GB), PSI
memory full65 % andio full53 % over 5 minutes,co-tenant services (postgres, n8n, others) evicted to swap, sshd accepting TCP
on :22 but dropping the session before the handshake completed.
The shim
~/.claude/shell-snapshots/snapshot-bash-*.sh:Why this is worth fixing rather than documenting
around any of these words" is a normal thing to write, and it is what a model
naturally writes when asked to check whether a fact is present in a file.
just exceeds its timeout and gets backgrounded. Nothing indicates memory.
That is why we re-ran it twice more.
grepis shadowed transparently, so neither the user nor the model has anysignal that a different engine with different characteristics is in play.
agent can evict everything else.
Suggested mitigations, in order of usefulness
instead of allocating without bound.
RLIMIT_AS) on the shim's subprocess, so theworst case is one dead process rather than a dead machine.
grepwhen compilation exceeds a budget.grep --versioninside a Claude CodeBash call does not look like GNU grep.