Bash tool's injected `grep` shadow silently drops .gitignore'd files (--ignore-files), making absence unprovable
Version: 2.1.237 (native) · macOS Darwin 25.5.0 · zsh 5.9
Summary
The shell snapshot injects a grep function routing to embedded ugrep with --ignore-files. This changes search semantics, not just the implementation: bare grep now honors .gitignore. Matches that exist on disk are omitted with exit code 0 and empty stderr, so a false "not found" is byte-identical to a true one.
Location
~/.claude/shell-snapshots/snapshot-zsh-*.sh, section # Shadow find/grep with embedded bfs/ugrep. In my 2302-line snapshot it lands at line 2247, i.e. after all user rc content (lines 4-2231), so it cannot be overridden from .zshrc. Regenerated on every CLI update.
ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I \
--exclude-dir=.git --exclude-dir=.svn --exclude-dir=.hg \
--exclude-dir=.bzr --exclude-dir=.jj --exclude-dir=.sl "$@"
Reproduction (verified on the above build)
mkdir -p /tmp/repro/build && cd /tmp/repro && git init -q
echo 'build/' > .gitignore
echo 'UNIQUE_TOKEN_123' > build/artifact.txt
echo 'UNIQUE_TOKEN_123' > tracked.txt
grep -r UNIQUE_TOKEN_123 . # 1 hit, exit 0, stderr empty
command grep -r UNIQUE_TOKEN_123 . # 2 hits, exit 0, stderr empty
Why this hits an agent harder than a human
A person reads grep output in context and generally remembers their build dir is ignored. The model uses bare grep to make presence/absence assertions that drive irreversible decisions: "this symbol has no other callers, safe to delete", "this config key is unused", "no secret is committed anywhere". A real .gitignore covers dist/, build/, node_modules/, .env, generated sources, vendored deps -- precisely where those questions get a different answer. Nothing in the result indicates filtering occurred.
The other flags in that line are fine. --exclude-dir=.git, -I, and --hidden are scope adjustments a caller can predict from the command name. --ignore-files differs in kind: the filter set is project-defined, invisible at the call site, and varies per directory.
Requested fix, in preference order
- Drop
--ignore-filesfrom the default. Extra hits are a recoverable error; missing hits are not. - If the default stays, make suppression audible -- one stderr line when matches are dropped, e.g.
ugrep: 3 files skipped by ignore rules (usecommand grepto include). The silence is the defect; a noisy version is acceptable. - Provide a supported opt-out (env var such as
CLAUDE_CODE_NO_SHELL_SHADOW=1, or a post-snapshot hook) so the shadowing can be disabled per machine. Today the only escape is rememberingcommand grepat every call site, and the shadow returns on each update.
Related, same injection block
find is shadowed with bfs, which rejects relative -newermt values that GNU/BSD find accepts. Lower severity since it errors rather than under-reporting, but the same root cause: a substitution that isn't behavior-identical, installed where the user can't reach it.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗