Bash tool: snapshot-injected ugrep/bfs/rg shim uses bare exec in subshell contexts — statements after the first bare call silently never run
Environment
- Claude Code 2.1.170 (VS Code extension 2.1.172, native binary), macOS (darwin arm64), bash
- Reproduced consistently in the Bash tool of an interactive session
Summary
The Bash tool's shell loads shimmed grep, find, and rg shell functions from ~/.claude/shell-snapshots/snapshot-bash-*.sh (routing to the claude binary's embedded ugrep/bfs/rg via ARGV0=..., variables prefixed _cc_). In the bash branch, when [[ $BASHPID != $$ ]] the function runs a bare exec -a ugrep "$_cc_bin" ... instead of the parenthesized ( exec ... ) form.
The Bash tool appears to execute commands in a context where BASHPID != $$, so the bare-exec branch is taken — and exec replaces the shell that is running the user's compound command. Result: in any multi-statement command, everything after the first bare grep/find/rg silently never executes.
Repro
Inside a Claude Code session, run via the Bash tool:
echo START; grep -in 'clean' /tmp/somefile.txt; echo END
Observed: output is START + the grep matches. END never prints. The tool's reported exit code is grep's exit code (so a no-match grep makes the whole command "fail" with exit 1, and any statements after it — including file writes and cleanup — never run).
Expected: all three statements run; END prints.
Control cases that behave correctly:
echo START; command grep -in 'clean' /tmp/somefile.txt; echo END→ all statements run (bypasses the function)- Plain pipelines (
ls | grep x) and a grep as the sole/last statement are unaffected - Same breakage with
find(bfs shim) andrg:find ~ -name x -maxdepth 1; echo after→afternever prints
type grep inside the tool shell confirms it's a function; the bash branch contains:
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
Impact
This is a silent failure: partial output, half-written redirect files (a { ...; } > file block stops mid-write), and misleading exit codes. In our case it caused a model session to draw false conclusions from truncated find/grep results (a file declared "doesn't exist" because the second statement of a compound never ran). It also seems likely to be a hidden cause of flaky "command failed" reports wherever models compose grep/find into multi-step commands.
Suggested fix
Use the subshell-wrapped ( exec ... ) form unconditionally (the BASHPID != $$ special case is only safe when the subshell contains nothing but the grep call, which isn't true when the whole compound script is already running in a subshell), or call the shim binary without exec.
Workaround we're using
A PreToolUse hook that blocks bare grep/find/rg in multi-statement commands and instructs the model to use command grep / command find.