[BUG] Shadowed grep silently skips gitignored files (--ignore-files): secret scans report clean while missing .env and *.log
What's Wrong?
The shell snapshot Claude Code generates replaces grep with a shell function that invokes the Claude binary in ugrep-compatible mode with a fixed flag set including --ignore-files. As a result, grep silently skips any file matched by .gitignore.
Because .env, *.log, credentials* and *.pem are exactly the files people gitignore, a grep -r used to check for leaked secrets reports nothing and exits 0 while the secret sits in an ignored file one directory down.
The failure is silent in the worst way: no warning, no stderr output, and the normal exit code. "I scanned and found nothing" is indistinguishable from "my scanner skipped the files that matter."
From the generated snapshot:
# Shadow find/grep with embedded bfs/ugrep
unalias find 2>/dev/null || true
unalias grep 2>/dev/null || true
...
function grep {
...
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 ${1+"$@"}
}
Two flags change grep semantics, and they are independent:
| Flag | Effect | Caller-side workaround |
|---|---|---|
| --ignore-files | Silently skips gitignored files | none |
| -I | Skips files heuristically classified as binary; a file with invalid UTF-8 gets dropped whole | -a restores it |
find and rg are shadowed the same way, but find's flag set does not include --ignore-files, so this report is specific to grep.
What Should Happen?
grep should behave like grep. A tool invoked under that name, with the exact argv the user typed, should not apply a gitignore filter the user did not request and cannot see.
Note this is not an argument against rg's behaviour. rg documents that it respects .gitignore, and users choose it knowing that. grep carries fifty years of POSIX semantics, and callers write commands against those semantics.
Steps to Reproduce
mkdir /tmp/repro && cd /tmp/repro && git init -q .
printf 'AKIA_FAKE_SECRET_MARKER\n' > tracked.txt
printf 'AKIA_FAKE_SECRET_MARKER\n' > app.log
printf 'AKIA_FAKE_SECRET_MARKER\n' > .env
printf '*.log\n.env\n' > .gitignore
git add -A
grep -r AKIA_FAKE_SECRET_MARKER . ; echo "rc=$?"
grep -ra AKIA_FAKE_SECRET_MARKER . ; echo "rc=$?"
/usr/bin/grep -r AKIA_FAKE_SECRET_MARKER . ; echo "rc=$?"
Error Messages/Logs
$ grep -r AKIA_FAKE_SECRET_MARKER .
tracked.txt:AKIA_FAKE_SECRET_MARKER
rc=0 <-- .env and app.log silently missing, rc=0
$ grep -ra AKIA_FAKE_SECRET_MARKER .
tracked.txt:AKIA_FAKE_SECRET_MARKER
rc=0 <-- -a does NOT help; it only addresses -I
$ /usr/bin/grep -r AKIA_FAKE_SECRET_MARKER .
./tracked.txt:AKIA_FAKE_SECRET_MARKER
./.env:AKIA_FAKE_SECRET_MARKER
./app.log:AKIA_FAKE_SECRET_MARKER
rc=0 <-- correct
Why this is security-relevant
The set of files --ignore-files hides correlates strongly with the set of files that hold secrets. A developer or an agent running
grep -rn 'AKIA\|BEGIN PRIVATE KEY\|xoxb-' .
as a pre-publish check gets a clean result and reasonably concludes there is nothing to redact. This is a check that fails open, silently, in the direction of disclosure.
This matters for agentic use in particular: an agent asked to "verify no credentials are present before we make this repo public" will run grep, get zero hits, and report success.
Scope and workarounds
LC_ALL=Cdoes not work around either flag.-aaddresses-Ionly. There is no caller-side workaround for--ignore-filesshort of bypassing the function (/usr/bin/grep,command grep).- Non-interactive scripts are largely unaffected:
bash script.shand#!/usr/bin/env bashscripts do not inherit the zsh function (bash -c 'type grep'resolves to/usr/bin/grep). The exposure is in interactive shells and anything run through a shell that sources the snapshot — which is where ad-hoc audit commands get typed.
Suggested fixes
- Drop
--ignore-filesfrom thegrepshadow. It changes which files exist from the caller's point of view; that is not a performance detail. - If the filtering is intentional, emit a one-line notice to stderr when files were skipped because of it (
N file(s) skipped by --ignore-files). That restores the ability to distinguish "no matches" from "not searched". - Provide a documented opt-out (e.g.
CLAUDE_CODE_NO_SHADOW_GREP=1) and mention the shadowing in the docs, so audit tooling can be written correctly.
Fix 1 alone resolves the security-relevant part.
Related issues
Other reports about the same shadowing mechanism, all describing different failure modes:
- #73718 — BRE alternation produces a regex parse error through the shim
- #67021 / #54394 — bundled ugrep OOMs on certain bounded intervals
- #67623 (closed) — shadowing killing bash mid-command; silent truncation
To my knowledge none covers --ignore-files hiding gitignored files, which is why this is filed separately.
Is this a regression?
Unknown — I have not tested older versions. The shadowing block is present in the snapshot generated by 2.1.220.
Claude Code Version
2.1.220
Platform
macOS 26.6, arm64, zsh
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗