[BUG] Embedded ugrep hangs indefinitely (pegged core) on unanchored bounded-repetition pattern — the same binary's embedded ripgrep finishes in 53 ms

Status Closed — duplicate
Reported on v2.1.233
Maintainer reply None cached
Activity 2 comments · opened Aug 16, 2026 · closed Aug 17, 2026

Environment

  • Claude Code 2.1.233 (native Linux build, x86_64)
  • The grep shell shim installed by Claude Code (routes to the embedded ugrep 7.5.0), also reachable through the agent-facing Grep tool
  • Reproduced on a plain 37 KB text file — no special encoding, no long lines beyond ~500 chars

Summary

A regex whose shape is [^<>]{0,80}(word1|word2|word3|word4)[^<>]{0,80} used with -o -i makes the embedded ugrep spin forever (killed manually after 20+ s on a 37 KB file; on first occurrence it ran for minutes and saturated a core). The same pattern on the same file completes in 0.053 s with the ripgrep 14.1.1 that the very same Claude Code binary already embeds, and in 0.23 s with GNU grep 3.12.

This is the match-time sibling of #54394 (which reports the memory-amplification side on WSL2): here there is no OOM, just a core pegged indefinitely on Linux with a tiny input.

Minimal reproduction (fully synthetic)

Generate a ~37 KB lorem-ipsum file (deterministic, seeded):

import random
random.seed(42)
words = ["lorem","ipsum","dolor","sit","amet","consectetur","adipiscing","elit",
         "sed","do","eiusmod","tempor","incididunt","labore","dolore","magna"]
out = []
for i in range(90):
    para = " ".join(random.choice(words) for _ in range(60))
    if i % 11 == 0:
        para += " gamma"
    out.append(f"<p>{para}</p>")
open("synthetic.html","w").write("\n".join(out))

Then, in any shell where the Claude Code grep shim is active (or via the Grep tool from an agent):

grep -o -i -E '[^<>]{0,80}(alpha|beta|gamma|delta)[^<>]{0,80}' synthetic.html

Observed vs. expected

| Engine | Command | Result |
|---|---|---|
| Embedded ugrep 7.5.0 (grep shim / Grep tool) | grep -o -i -E PATTERN synthetic.html | Hangs — still running when killed at 20+ s, core pegged |
| Embedded ripgrep 14.1.1 (same Claude Code binary, rg shim) | rg -o -i PATTERN synthetic.html | 0.053 s, 9 correct matches |
| GNU grep 3.12 (/usr/bin/grep) | grep -o -i -E PATTERN synthetic.html | 0.23 s, 9 matches |

Expected: milliseconds on a 37 KB file, or a fast, explicit error if the engine considers the pattern too expensive.

Why this pattern is pathological for ugrep specifically

  • It has no literal anchor: it starts with an optional bounded class, so every byte offset is a viable match start — the literal-prefilter fast path is defeated entirely.
  • Bounded repetition {0,80} on both sides of an alternation, case-insensitively, inflates the automaton (81 × 81 widths × 4 alternates × case folding).
  • With -o, POSIX leftmost-longest semantics force the engine to explore the full 80-char extensions at every candidate position.

Engines with linear-time guarantees (Rust regex, which additionally fail-fasts with a "compiled regex exceeds size limit" error when a pattern is too big) and GNU grep's lazy DFA both handle it instantly. Note that this pattern shape is exactly what LLM agents produce when they try to capture ±N characters of context around a keyword in a single regex — in our case it was generated by one of Claude Code's own subagents during routine document analysis. The tool is exposed to this shape in normal operation, not just adversarially, and the agent has no way to know its search primitive lacks a linear-time guarantee.

Notes that may help (offered without knowledge of the internals)

We don't know why ugrep was chosen as the backend — it may well cover features ripgrep lacks (BRE via -G, fuzzy matching, compressed-archive search) — so we won't presume to prescribe a fix. Two data points for whoever does know the implementation:

  • The same Claude Code binary already embeds ripgrep 14.1.1, and it completes this exact repro in 0.053 s. But ripgrep has no BRE mode, and the shim currently forces -G (#59517), so a straight engine swap may not be behavior-compatible with existing usage.
  • Rust's regex crate fails fast with an explicit "compiled regex exceeds size limit" error rather than hanging. An analogous guard around the embedded ugrep (a compile-size budget, or a watchdog on the match loop) would turn this failure mode from a silently pegged core into an actionable error, whatever engine stays underneath.

Related issues

  • #54394 — embedded ugrep wrapper amplifies regex backtracking into V8-heap OOM (WSL2). Same root family; this report is the pure-CPU Linux variant with a deterministic 30-second repro.
  • #81916 — bundled ugrep busy-loops at 100% CPU when --ignore-files resolves to a directory.
  • #59517 — grep shim silently overrides -E with -G.

View original on GitHub ↗

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