[BUG] Shell grep shim: BRE alternation \| combined with literal ( produces 'rg: regex parse error: unclosed group'

Open 💬 0 comments Opened Jul 3, 2026 by jbylund

Summary

The shell-snapshot grep function routes grep invocations to the embedded ugrep-style BRE emulation (backed by ripgrep's Rust regex engine). Its BRE-to-Rust-regex translation breaks when a pattern combines BRE alternation (\|) with a literal unescaped (: the alternation is converted and wrapped in (?:...), but the literal paren is not escaped, producing an invalid regex.

Each feature works fine on its own — the combination fails.

Environment

  • Claude Code v2.1.199
  • macOS 26.5 (Darwin 25.5.0), zsh

Minimal repro

Inside any Claude Code Bash tool session:

$ printf 'foo\nbar\n' > /tmp/greptest.txt

$ grep -n "foo\|bar" /tmp/greptest.txt      # BRE alternation alone: works
1:foo
2:bar

$ grep -n "foo(" /tmp/greptest.txt          # literal paren alone: works (no match, exit 1, no error)

$ grep -n "foo(\|bar" /tmp/greptest.txt     # combination: parse error
rg: regex parse error:
    (?:foo(|bar)
    ^
error: unclosed group
0 matches for 'foo(\|bar'

The translated pattern shown in the error, (?:foo(|bar), makes the bug visible: \| was correctly converted to | and the branches wrapped in a non-capturing group, but the literal ( in the first branch was left unescaped, so it opens a group that never closes.

Expected behavior

Under BRE semantics (the shim runs with -G), an unescaped ( is a literal character. grep "foo(\|bar" should translate to (?:foo\(|bar) and match lines containing foo( or bar — as /usr/bin/grep does:

$ command grep -n "foo(\|bar" /tmp/greptest.txt
2:bar

Actual behavior

Exit code 2 with rg: regex parse error: ... unclosed group.

Impact

This surfaces in real usage when the model greps for function-call sites with multiple alternatives, e.g.:

grep -n "\.matches(\|card_to_pydict(\|run_query_linear(" src/lib.rs

fails with the same unclosed-group error, and the rg: prefix in the message misleads users (and the model) into blaming a local ripgrep alias rather than the shim.

Notes

  • Likely the same translation applies to other BRE metacharacters (), and possibly {/}) when combined with \| — the paren case is the one verified.
  • Related but distinct: #54394 concerns the same embedded ugrep wrapper but a different failure (regex backtracking OOM).

View original on GitHub ↗