Bash tool: grep→ugrep shim exits 2 when one operand is missing even though another matched (GNU grep/POSIX -q exits 0)

Status Open
Reported on v2.1.231
Maintainer reply None cached
Activity 1 comment · opened Aug 13, 2026

Summary

The Bash tool's grep shim (the shell function installed via the shell snapshot, which execs $CLAUDE_CODE_EXECPATH -G with ARGV0=ugrep) returns exit status 2 when one operand file is missing, even though a line was selected in another operand. GNU grep returns 0 in that situation, and POSIX specifies that behaviour explicitly for -q:

If the -q option is specified, the exit status shall be zero if an input line is selected, even if an error was detected.

-s does not suppress it. The result is that a very common shell idiom — grepping a config file plus an optional *.local.* override — silently takes the wrong branch whenever an agent runs it, while a human running the identical line in their own shell gets the correct result.

Repro

Run these directly in Claude Code's Bash tool (not inside a bash script.sh — the shim is a shell function, so it does not apply in a non-interactive script, which is what makes this easy to misdiagnose):

mkdir -p /tmp/grepshim && echo needle > /tmp/grepshim/present.txt && rm -f /tmp/grepshim/missing.txt

grep -q needle /tmp/grepshim/present.txt /tmp/grepshim/missing.txt 2>/dev/null; echo "shim      rc=$?"
command grep -q needle /tmp/grepshim/present.txt /tmp/grepshim/missing.txt 2>/dev/null; echo "GNU grep  rc=$?"
grep -sq needle /tmp/grepshim/present.txt /tmp/grepshim/missing.txt; echo "shim -s   rc=$?"
grep -q needle /tmp/grepshim/present.txt 2>/dev/null; echo "shim 1arg rc=$?"

Observed:

shim      rc=2      <-- wrong
GNU grep  rc=0
shim -s   rc=2      <-- -s does not help
shim 1arg rc=0      <-- single operand is fine

Expected: the first line returns 0, matching command grep.

Operand order does not matter — GNU grep returns 0 whether the missing file is first or second.

Environment

  • Claude Code 2.1.231
  • Linux (Ubuntu, kernel 6.8)
  • System grep: GNU grep 3.11 (/usr/bin/grep)
  • type grep resolves to the snapshot-installed shell function, not /usr/bin/grep

Why this matters

This is not cosmetic — it inverts a boolean that scripts branch on, and it fails silently. The real case I hit: a slash-command skill detects whether a repo enables a plugin with

grep -qh 'plugin@marketplace' .claude/settings.json .claude/settings.local.json 2>/dev/null

.claude/settings.local.json usually doesn't exist, so under the shim this always reported "not enabled". Every agent-run invocation of that command took the wrong code path and produced output in the wrong format. Nothing errored, nothing logged, and it was invisible to a human testing the same line by hand.

Any script doing grep -q <pattern> <file> <optional-override-file> is exposed — a widespread pattern for settings.local.json, .env.local, *.override.* and similar. Debugging it leads investigators to blame GNU grep or their own script (I did both) before thinking to run type grep.

Workaround

Read the operands through stdin, which bypasses the shim's operand handling and behaves identically under both engines:

cat .claude/settings.json .claude/settings.local.json 2>/dev/null | grep -q 'pattern'

Single-operand greps are also unaffected, so ORing separate invocations works too.

Suggested fix

Make the shim's exit status match GNU grep / POSIX: when -q is given and a line was selected, exit 0 regardless of per-file errors. More generally, a file-not-found error should not mask a successful match across the other operands.

Related

  • #67361 — same shim, different failure mode (bare exec in subshell contexts).

View original on GitHub ↗

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