Bundled ugrep runs away (4.7GB, 4h) on bounded-repeat pattern over long lines; complexity guard does not fire

Status Fixed / completed
Reported on v2.1.233
Maintainer reply None cached
Activity 1 comment · opened Aug 19, 2026 · closed Aug 19, 2026
Attribution: This issue was identified, investigated and written by Claude Opus 5 (Claude Code), and submitted by Opus 5 at the request and on behalf of the account owner.

Bundled ugrep runs away on a bounded-repeat pattern, and the complexity guard does not fire

Summary. A grep issued through the Bash tool consumed 4.7 GB RSS and ran for
3 h 58 m without completing, wedging the session. The same guard that should have
rejected the pattern does fire on other patterns, so the limiter exists but did
not catch this case.

What ran. Claude Code routes grep to its bundled ugrep. The invocation was
shaped like:

ugrep -G -o "[^\"]\{0,120\}SOME_LITERAL[^\"]\{0,200\}" transcript.jsonl

Two bounded repeats of a negated character class either side of a literal, with
-o, in POSIX BRE mode (-G).

The input is the aggravating factor. The target was a session transcript
.jsonl — ~4 MB, but each line is a single JSON object, so individual lines reach
megabytes. With -o, the scan appears to go quadratic per line.

Observed.

  • 4,706 MiB RSS (sampled once a minute for four hours; it grew steadily)
  • 62% CPU sustained, never completed
  • Twice triggered a system-wide OOM kill on earlier runs (5.1 GB and 5.4 GB)
  • The process is unkillable-by-waiting: only an explicit kill recovered it

The part that makes this a bug rather than a caveat. Later, a similar
bounded-repeat pattern was rejected outright:

ugrep: error: error at position 88
x80-\xbf][\x80-\xbf]*){0,80}
                            \___exceeds complexity limits

So a complexity limiter is present and does reject some patterns, but the
pathological case above sailed past it. Whatever threshold it applies does not
account for the interaction between bounded repeats, -o, and very long lines.

Suggested fixes, roughly in order of preference:

  1. Extend the complexity check to cover this pattern class, so it fails fast

with the same clear error instead of consuming GBs.

  1. Apply a memory ceiling to the bundled grep and abort with a diagnostic.
  2. Special-case very long lines, which are common in the .jsonl transcripts

Claude Code itself writes and therefore very likely to be grepped.

Repro sketch. Generate a .jsonl whose lines are ~1 MB single JSON objects,
then run the invocation above against it and watch RSS.

Diagnostic note for whoever picks this up. The bundled binary lives at
~/.local/share/claude/versions/<version>, so its comm is the version string.
In OOM reports and ps output it appears as e.g. 2.1.233, which reads like an
unrelated process. That cost several hours of misdiagnosis — the kernel's OOM
report names a victim that looks like nothing on the system. A more descriptive
comm/argv[0] would help a lot.

---

Consequence: A hung foreground Bash command leaves the session unresponsive with no signal

Summary / question. The runaway in §1 blocked a single turn for 3 h 58 m.
Throughout, every external health signal read healthy. I am unsure what timeout
behaviour is intended here, so this is raised as a question as much as a report.

Observed. During the hang:

  • the process was alive and responsive to input
  • the channel plugin kept polling and delivered incoming messages
  • messages were visibly received and queued behind the stuck turn
  • nothing timed out, and nothing was surfaced to the user

From the user's side the assistant simply never answered, for four hours, while
appearing to run normally. Recovery required killing the child shell externally,
after which the turn completed normally and the queued messages were answered —
so the session state itself was fine the whole time.

Questions.

  1. Is there an intended ceiling on a foreground Bash tool invocation? If so, it

did not apply here.

  1. Could a turn exceeding some duration surface a warning, rather than being

indistinguishable from ordinary work?

  1. For channel-driven sessions specifically: is there a way to signal

"still working, N minutes elapsed" so an unattended session does not read as
dead?

Note on a mitigation that backfired, in case it is useful design input. A
MemoryHigh= cgroup limit was applied to contain the runaway. It worked exactly
as documented — and made things worse. Instead of being OOM-killed (loud, and
recoverable by an existing watchdog in five minutes), the process was throttled
into swap and ground on for four hours (silent, unrecoverable without manual
intervention). MemoryMax= plus a swap cap gives the better failure. The general
lesson: for an unattended agent, a fast, loud failure beats a slow, quiet one.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗