Add an opt-out for the built-in find→bfs / grep→ugrep shadow functions injected into the Bash tool shell
Title: Add an opt-out for the built-in find→bfs / grep→ugrep shadow functions injected into the Bash tool shell
What happens
The Bash tool's shell snapshot (~/.claude/shell-snapshots/snapshot-zsh-*.sh) injects shadow functions that replace find and grep with bundled bfs / ugrep binaries:
# Shadow find/grep with embedded bfs/ugrep
unalias find 2>/dev/null || true
unalias grep 2>/dev/null || true
function find {
...
ARGV0=bfs "$_cc_bin" -S dfs -regextype findutils-default "$@"
}
function grep {
...
ARGV0=ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"
}
These are not user aliases — they are emitted by Claude Code itself and run on every Bash tool call.
Why it's a problem
bfs and ugrep are not drop-in for GNU find/grep:
- bfs leading-dash paths:
find ./-somedir -name '*.x'(a directory whose name starts with-) fails under bfs — it parses the path as an unknown flag (Unknown argument; did you mean -ignore_readdir_race?). The same command works with GNU find. This silently breaks agent-generated commands. - ugrep
--ignore-files: makesgrephonor.gitignoreby default, so a literalgrepover a repo silently skips ignored files — different result set than GNU grep, no error, no signal.
The agent (and users) reasonably assume find/grep mean the system tools. The workaround is to type \find / \grep / command find everywhere, which is easy to forget and pollutes generated commands.
What I'm asking for
An opt-out, any of:
- An env var, e.g.
CLAUDE_CODE_DISABLE_BUILTIN_SEARCH=1, that skips emitting the find/grep shadow functions into the snapshot. - A
settings.jsonkey (e.g."disableBuiltinSearchShadows": true). - Failing a global toggle, make the shadows opt-in rather than opt-out.
Environment
- Claude Code 2.1.183, macOS (darwin), zsh 5.9
- Confirmed no existing env/setting disables this (grepped the version dir for
BFS/UGREP/SHADOWtoggles — none found).
Related
Snapshot also serializes user ~/.zshrc aliases (cat=bat, ls=eza, cp -i, …) into the non-interactive tool shell; users can guard those with [[ -o interactive ]], but the find/grep shadows above are CC-injected and have no such escape hatch.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Not a duplicate. #60325 (broken ARGV0 dispatch → nested agent) and #67623 (mid-command shell kill + false ENOSPC banner) are closed crash bugs; #62642 is a bash<4.0 shell-termination crash. They report specific failures of the shadow mechanism.
This issue is a feature request for an opt-out toggle, motivated not by a crash but by semantic divergence between the embedded tools and their GNU counterparts:
bfsparses leading-dash paths differently from GNU find, sofind ./-somedir -name '*.x'fails (Unknown argument) where stock find works.ugrep's default--ignore-filesmakes a literalgrepsilently honor.gitignore— a different result set with no error.A disable switch (env var or
settings.jsonkey) would be the umbrella mitigation: it makes every shadow-related failure above avoidable for users who just want stockfind/grepsemantics. Keeping this open as the consolidated opt-out request.+1 for the opt-out. Adding a failure mode of the
grepshadow we hit today that I have not seen reported elsewhere: process substitution silently returns empty.Repro (macOS Darwin 25.5.0, zsh 5.9, Claude Code 2.1.212, inside the Bash tool):
No error message, no non-zero visible result — anything like
n=$(grep -c … <(git show REV:file))just yields an empty string, which then propagates through script logic unnoticed.This compounds with the already-reported forced
-Goverride (#59517) and the--ignore-files/-Idefault result-set differences. All of these share the same shape: silent divergence from GNU/BSD grep semantics in non-interactive/scripted use.Until an opt-out exists, our workaround is a hard team rule: pipelines and script logic must use
command grep/\grep; baregreponly for interactive human-read output.Two measured consequences of the injected
grepshadow (Claude Code 2.1.185, macOS, zsh snapshot), adding to the case for an opt-out:1.
-vexit-code semantics diverge from POSIX/GNU grep.The same expression evaluated inside the Bash tool therefore returns the opposite answer locally vs. in CI (GNU grep). We caught this only because the expression gated a CI path-filter decision: an agent-authored
grep -qvE-based check classified a mixed code+docs change as docs-only locally while behaving correctly on the runner. Related to #59517.2.
--ignore-filessilently excludes gitignored files — secret sweeps report false-clean.Agent-driven audits (leaked-credential sweeps, PII hunts, "is this string referenced anywhere" checks) run exactly this shape of command, and "no matches" reads as "clean". There is no indication in the output that gitignored files were skipped.
Both are silent-divergence failure modes rather than ergonomic ones: the shadow only affects the Bash tool's interactive shell (scripts run via
bash script.shdon't inherit the function), which is precisely the unguarded ad-hoc layer.Until an opt-out exists, the workaround we've adopted:
command grepfor anything judgment-bearing, plus a SessionStart hook that detectsARGV0=ugrepin the newest~/.claude/shell-snapshots/snapshot-*.shand warns the model at session start. +1 forCLAUDE_CODE_DISABLE_BUILTIN_SEARCH(or at minimum: don't pass--ignore-fileswhen invoked under the namegrep, and match POSIX exit semantics).