claude-code - 💡(How to fix) Fix [Bug] grep shell wrapper silently overrides -E (ERE) with -G (BRE) via ugrep — breaks scripts using extended regex

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Claude Code's shell environment overrides grep with a bash function that routes all calls through the Claude Code binary using exec -a ugrep. The wrapper unconditionally passes -G (basic regular expressions / BRE) to ugrep, regardless of what flags the caller specified. When a script passes -E (extended regular expressions / ERE), the -G override causes the regex to be interpreted as BRE, producing wrong results with no error message.

In non-interactive subshell contexts ($BASHPID != $$), the exit code returned is 127, making the call appear as "command not found" rather than a regex mismatch.

This silently breaks any script that uses grep -E in the Claude Code shell — the command returns incorrect results or exits 127 without any visible indication that the regex engine changed.

This is distinct from #57362 (the $ZSH_VERSION unbound variable issue, which is cosmetic). This is a correctness bug: wrong regex engine, wrong results, no error.


Error Message

Claude Code's shell environment overrides grep with a bash function that routes all calls through the Claude Code binary using exec -a ugrep. The wrapper unconditionally passes -G (basic regular expressions / BRE) to ugrep, regardless of what flags the caller specified. When a script passes -E (extended regular expressions / ERE), the -G override causes the regex to be interpreted as BRE, producing wrong results with no error message. This is distinct from #57362 (the $ZSH_VERSION unbound variable issue, which is cosmetic). This is a correctness bug: wrong regex engine, wrong results, no error.

  • No error is printed — the command appears to run but produces incorrect output

Root Cause

Claude Code's shell environment overrides grep with a bash function that routes all calls through the Claude Code binary using exec -a ugrep. The wrapper unconditionally passes -G (basic regular expressions / BRE) to ugrep, regardless of what flags the caller specified. When a script passes -E (extended regular expressions / ERE), the -G override causes the regex to be interpreted as BRE, producing wrong results with no error message.

In non-interactive subshell contexts ($BASHPID != $$), the exit code returned is 127, making the call appear as "command not found" rather than a regex mismatch.

This silently breaks any script that uses grep -E in the Claude Code shell — the command returns incorrect results or exits 127 without any visible indication that the regex engine changed.

This is distinct from #57362 (the $ZSH_VERSION unbound variable issue, which is cosmetic). This is a correctness bug: wrong regex engine, wrong results, no error.


Fix Action

Fix / Workaround

  • Any bash script sourced or executed in the Claude Code shell that uses grep -E silently produces wrong results
  • ERE features that break under BRE: alternation (a|b), +, ?, {n,m}, character class shortcuts
  • No error is printed — the command appears to run but produces incorrect output
  • Exit code 127 in subshell context mimics "command not found," not a regex failure
  • Workaround is non-obvious: unset -f grep or command grep -E

Code Example

# 1. Confirm grep is overridden and -G is hardcoded
declare -f grep | grep '\-G'
# Output shows: exec -a ugrep "$_cc_bin" -G --ignore-files ... "$@"
# -G is hardcoded before "$@" — your -E is ignored

# 2. ERE alternation fails silently
echo "requirements-dev.txt" | grep -qE '(^|[-_.])(dev)([-_.]|$)' && echo "MATCH" || echo "NO MATCH"
# Expected: MATCH
# Actual:   NO MATCH  (BRE interprets | as literal, not alternation)

# 3. ERE character class fails silently
echo "foo-bar" | grep -qE 'foo[-_]bar' && echo "MATCH" || echo "NO MATCH"
# Expected: MATCH
# Actual:   NO MATCH or exit 127 in subshell

# 4. command grep works correctly, confirming the wrapper is the cause
echo "requirements-dev.txt" | command grep -qE '(^|[-_.])(dev)([-_.]|$)' && echo "MATCH" || echo "NO MATCH"
# Output: MATCH

# 5. Exit code 127 in subshell context
result=$(echo "test-file.txt" | grep -qE '[-_]'; echo $?)
echo "exit: $result"
# Expected: 0 (match found)
# Actual:   127 (exec -a ugrep failed in subshell)

---

grep ()
{
    local _cc_bin="${CLAUDE_CODE_EXECPATH:-}";
    [[ -x $_cc_bin ]] || _cc_bin=/home/user/.local/bin/claude;
    ...
    if [[ $BASHPID != $$ ]]; then
        exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@";
    else
        ( exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@" );
    fi
}

---

for _cc_a in "$@"; do
  case "$_cc_a" in
    -E|-P|-F|-G|--extended-regexp|--perl-regexp|--fixed-strings|--basic-regexp)
      # Caller specified a mode — pass through without forcing -G
      exec -a ugrep "$_cc_bin" --ignore-files --hidden -I --exclude-dir=.git ... "$@"
      return ;;
  esac
done
# No mode flag — safe to default to -G
exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"
RAW_BUFFERClick to expand / collapse

Summary

Claude Code's shell environment overrides grep with a bash function that routes all calls through the Claude Code binary using exec -a ugrep. The wrapper unconditionally passes -G (basic regular expressions / BRE) to ugrep, regardless of what flags the caller specified. When a script passes -E (extended regular expressions / ERE), the -G override causes the regex to be interpreted as BRE, producing wrong results with no error message.

In non-interactive subshell contexts ($BASHPID != $$), the exit code returned is 127, making the call appear as "command not found" rather than a regex mismatch.

This silently breaks any script that uses grep -E in the Claude Code shell — the command returns incorrect results or exits 127 without any visible indication that the regex engine changed.

This is distinct from #57362 (the $ZSH_VERSION unbound variable issue, which is cosmetic). This is a correctness bug: wrong regex engine, wrong results, no error.


Repro

# 1. Confirm grep is overridden and -G is hardcoded
declare -f grep | grep '\-G'
# Output shows: exec -a ugrep "$_cc_bin" -G --ignore-files ... "$@"
# -G is hardcoded before "$@" — your -E is ignored

# 2. ERE alternation fails silently
echo "requirements-dev.txt" | grep -qE '(^|[-_.])(dev)([-_.]|$)' && echo "MATCH" || echo "NO MATCH"
# Expected: MATCH
# Actual:   NO MATCH  (BRE interprets | as literal, not alternation)

# 3. ERE character class fails silently
echo "foo-bar" | grep -qE 'foo[-_]bar' && echo "MATCH" || echo "NO MATCH"
# Expected: MATCH
# Actual:   NO MATCH or exit 127 in subshell

# 4. command grep works correctly, confirming the wrapper is the cause
echo "requirements-dev.txt" | command grep -qE '(^|[-_.])(dev)([-_.]|$)' && echo "MATCH" || echo "NO MATCH"
# Output: MATCH

# 5. Exit code 127 in subshell context
result=$(echo "test-file.txt" | grep -qE '[-_]'; echo $?)
echo "exit: $result"
# Expected: 0 (match found)
# Actual:   127 (exec -a ugrep failed in subshell)

Wrapper (from declare -f grep)

grep ()
{
    local _cc_bin="${CLAUDE_CODE_EXECPATH:-}";
    [[ -x $_cc_bin ]] || _cc_bin=/home/user/.local/bin/claude;
    ...
    if [[ $BASHPID != $$ ]]; then
        exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@";
    else
        ( exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@" );
    fi
}

The -G flag is hardcoded before "$@". ugrep uses the last-specified mode flag — when the caller passes both -G and -E, BRE wins, silently switching the engine.


Expected behavior

grep -E 'pattern' in the Claude Code shell should use extended regular expressions. If the wrapper routes through ugrep, it should respect the caller's -E/-P/-F flags rather than hardcoding -G.

Minimal fix: detect the caller's regex mode flag before adding -G:

for _cc_a in "$@"; do
  case "$_cc_a" in
    -E|-P|-F|-G|--extended-regexp|--perl-regexp|--fixed-strings|--basic-regexp)
      # Caller specified a mode — pass through without forcing -G
      exec -a ugrep "$_cc_bin" --ignore-files --hidden -I --exclude-dir=.git ... "$@"
      return ;;
  esac
done
# No mode flag — safe to default to -G
exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"

Impact

  • Any bash script sourced or executed in the Claude Code shell that uses grep -E silently produces wrong results
  • ERE features that break under BRE: alternation (a|b), +, ?, {n,m}, character class shortcuts
  • No error is printed — the command appears to run but produces incorrect output
  • Exit code 127 in subshell context mimics "command not found," not a regex failure
  • Workaround is non-obvious: unset -f grep or command grep -E

Environment

  • Claude Code CLI (claude-sonnet-4-6)
  • Shell: bash
  • OS: Ubuntu 24 (Linux 6.14.0)
  • Reproduces in any Bash tool call that sources a bash script using grep -E

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

grep -E 'pattern' in the Claude Code shell should use extended regular expressions. If the wrapper routes through ugrep, it should respect the caller's -E/-P/-F flags rather than hardcoding -G.

Minimal fix: detect the caller's regex mode flag before adding -G:

for _cc_a in "$@"; do
  case "$_cc_a" in
    -E|-P|-F|-G|--extended-regexp|--perl-regexp|--fixed-strings|--basic-regexp)
      # Caller specified a mode — pass through without forcing -G
      exec -a ugrep "$_cc_bin" --ignore-files --hidden -I --exclude-dir=.git ... "$@"
      return ;;
  esac
done
# No mode flag — safe to default to -G
exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING