Summary
The v2.1.117 change ("Native builds on macOS and Linux: the Glob and Grep tools are replaced by embedded bfs and ugrep available through the Bash tool") routes every grep shell invocation through claude.exe (via exec -a ugrep in the shell-snapshot wrapper). Each grep therefore runs inside a fresh claude.exe process carrying its full V8 heap (~200 MB idle, 8 GB ceiling).
When a regex with catastrophic-backtracking shape runs against a long line (multi-MB minified HTML, a long line in a session jsonl, etc.), V8 holds the match buffer and RSS climbs to 8–15 GB before either the V8 ceiling fires or the host runs out of RAM. Pre-2.1.117, the same bad regex against the same input would run in system GNU grep (a small C process, ~30 MB peak), be slow, and the user would Ctrl+C. Post-2.1.117, same shape, same input, the host freezes on swap thrash until manual reboot.
This has caused 3 host freezes in 3 days on my WSL2 box.
Reproduction
Minimum failing case (works against any multi-MB single-line HTML body):
# Get a representative single-line minified HTML body (anything 1+ MB will do)
curl -sk -o /tmp/big.html https://www.meetscoresonline.com/ # or any single-line minified site
# Run a regex with multiple unbounded quantifiers — the shape that catastrophically backtracks
grep -oE '"a":"[^"]*"|"b":"[^"]*"|"c":"[^"]*"|"d":"[^"]*"|"e":"[^"]*"|"f":"[^"]*"|"g":"[^"]*"' /tmp/big.html | head -20
Watch top / ps. The grep process (which is actually claude.exe-as-ugrep under the wrapper) climbs to 8+ GB RSS within seconds. On a small box, swap fills, host freezes.
In production, this happens via Claude (the model) writing patterns like:
curl -sk "$URL" 2>&1 | grep -oE '"EventName":"[^"]*"|"MeetCity":"[^"]*"|"MeetState":"[^"]*"|"HostClub":"[^"]*"|"StatusText":"[^"]*"|"meetfromdate":"[^"]*"|"meettodate":"[^"]*"|2026 AAU MV[^<\"]*' | head -20
— a perfectly reasonable-looking JSON-extraction pattern. The model has no way to know that the multiple [^"]* alternations turn pathological under ugrep on a long line, because in every prior environment (including pre-2.1.117 Claude Code), the same pattern would just be slow, not lethal.
Expected vs Actual
Expected: A regex with bad shape is slow but bounded — at worst, the user Ctrl+Cs and the grep process dies with no host impact.
Actual: The grep process is claude.exe with V8 heap. Heap balloons to 8+ GB. On hosts with <16 GB RAM, swap saturates and the host becomes unresponsive. On hosts with more RAM, the V8 ceiling at ~8.2 GB triggers an internal OOM. Either way: significantly worse outcome than pre-wrapper grep.
Multiple existing issues report related "memory leak" / OOM symptoms — #4953, #11155, #25926, #27421, #30470 among others. I believe a meaningful fraction of these are this same root cause: V8 heap holding a tool result that wouldn't have been a problem in C-process grep.
Environment
- Claude Code 2.1.119 → 2.1.121 (issue first reproduced 2026-04-26 on 2.1.119; reproduced again on 2.1.121 today)
- WSL2, Linux 6.6.87.2-microsoft-standard-WSL2, systemd 255, cgroup v2
- Ubuntu 24.04
- 19 GB RAM, 8 GB swap
- Node v22.20.0
- Bash (zsh has the same issue per the snapshot's parallel branch)
Root Cause
The shell snapshot at ~/.claude/shell-snapshots/snapshot-bash-*.sh defines (verbatim):
function grep {
local _cc_bin=\"\${CLAUDE_CODE_EXECPATH:-}\"
[[ -x \$_cc_bin ]] || _cc_bin=/home/goduk/.local/bin/claude
if [[ ! -x \$_cc_bin ]]; then command grep \"\$@\"; return; fi
if [[ -n \$ZSH_VERSION ]]; then
ARGV0=ugrep \"\$_cc_bin\" -G --ignore-files --hidden -I --exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg --exclude-dir=.bzr --exclude-dir=.jj --exclude-dir=.sl \"\$@\"
elif ...
else
( exec -a ugrep \"\$_cc_bin\" -G --ignore-files --hidden -I --exclude-dir=.git --exclude-dir=.svn ... \"\$@\" )
fi
}
The intent — gitignore-aware, binary-skipping search via embedded ugrep — is good for performance. The unintended consequence is that V8's process memory profile is now the per-grep memory profile.
Suggested fixes (in increasing order of effort)
-
Run the embedded matcher under a memory rlimit / setrlimit guard before invoking the search loop. Cap RSS at, say, 1 GB for matcher invocations. ugrep itself needs nowhere near that for any legitimate search; the ceiling exists only to bound pathological backtracking.
-
Switch the embedded matcher to a non-backtracking engine (RE2, Hyperscan, or Rust's regex crate). ripgrep already does this — it's the default for the Bash tool's rg shim per the changelog. Aligning ugrep on RE2 (or replacing it with rg for the grep shim too) would eliminate the catastrophic-backtracking class entirely while preserving feature parity.
-
Document the failure mode in the changelog and recommend a user-side cgroup wrapper (others have shipped one — see zenn.dev/tjst_t/articles/260219-claude-code-cgroup-memory-limit). Short-term mitigation while a real fix lands.
Related issues
Workaround I'm shipping locally
While waiting for an upstream fix, I'm wrapping every claude.exe invocation in a systemd-run --user --scope cgroup with MemoryMax=2G for matcher invocations and MemoryMax=5G for sessions, plus a PreToolUse hook that detects multi-quantifier regex shapes and refuses to run them. I'll happily share details if useful for the upstream fix.
Summary
The v2.1.117 change ("Native builds on macOS and Linux: the
GlobandGreptools are replaced by embeddedbfsandugrepavailable through the Bash tool") routes everygrepshell invocation throughclaude.exe(viaexec -a ugrepin the shell-snapshot wrapper). Eachgreptherefore runs inside a freshclaude.exeprocess carrying its full V8 heap (~200 MB idle, 8 GB ceiling).When a regex with catastrophic-backtracking shape runs against a long line (multi-MB minified HTML, a long line in a session jsonl, etc.), V8 holds the match buffer and RSS climbs to 8–15 GB before either the V8 ceiling fires or the host runs out of RAM. Pre-2.1.117, the same bad regex against the same input would run in system GNU grep (a small C process, ~30 MB peak), be slow, and the user would Ctrl+C. Post-2.1.117, same shape, same input, the host freezes on swap thrash until manual reboot.
This has caused 3 host freezes in 3 days on my WSL2 box.
Reproduction
Minimum failing case (works against any multi-MB single-line HTML body):
Watch
top/ps. Thegrepprocess (which is actuallyclaude.exe-as-ugrepunder the wrapper) climbs to 8+ GB RSS within seconds. On a small box, swap fills, host freezes.In production, this happens via Claude (the model) writing patterns like:
— a perfectly reasonable-looking JSON-extraction pattern. The model has no way to know that the multiple
[^"]*alternations turn pathological under ugrep on a long line, because in every prior environment (including pre-2.1.117 Claude Code), the same pattern would just be slow, not lethal.Expected vs Actual
Expected: A regex with bad shape is slow but bounded — at worst, the user Ctrl+Cs and the grep process dies with no host impact.
Actual: The grep process is
claude.exewith V8 heap. Heap balloons to 8+ GB. On hosts with <16 GB RAM, swap saturates and the host becomes unresponsive. On hosts with more RAM, the V8 ceiling at ~8.2 GB triggers an internal OOM. Either way: significantly worse outcome than pre-wrapper grep.Multiple existing issues report related "memory leak" / OOM symptoms — #4953, #11155, #25926, #27421, #30470 among others. I believe a meaningful fraction of these are this same root cause: V8 heap holding a tool result that wouldn't have been a problem in C-process grep.
Environment
Root Cause
The shell snapshot at
~/.claude/shell-snapshots/snapshot-bash-*.shdefines (verbatim):The intent — gitignore-aware, binary-skipping search via embedded ugrep — is good for performance. The unintended consequence is that V8's process memory profile is now the per-grep memory profile.
Suggested fixes (in increasing order of effort)
Run the embedded matcher under a memory rlimit / setrlimit guard before invoking the search loop. Cap RSS at, say, 1 GB for matcher invocations. ugrep itself needs nowhere near that for any legitimate search; the ceiling exists only to bound pathological backtracking.
Switch the embedded matcher to a non-backtracking engine (RE2, Hyperscan, or Rust's
regexcrate). ripgrep already does this — it's the default for the Bash tool'srgshim per the changelog. Aligning ugrep on RE2 (or replacing it withrgfor thegrepshim too) would eliminate the catastrophic-backtracking class entirely while preserving feature parity.Document the failure mode in the changelog and recommend a user-side cgroup wrapper (others have shipped one — see zenn.dev/tjst_t/articles/260219-claude-code-cgroup-memory-limit). Short-term mitigation while a real fix lands.
Related issues
Workaround I'm shipping locally
While waiting for an upstream fix, I'm wrapping every
claude.exeinvocation in asystemd-run --user --scopecgroup withMemoryMax=2Gfor matcher invocations andMemoryMax=5Gfor sessions, plus a PreToolUse hook that detects multi-quantifier regex shapes and refuses to run them. I'll happily share details if useful for the upstream fix.