Add an opt-out for the built-in find→bfs / grep→ugrep shadow functions injected into the Bash tool shell

Status Open
Reported on v2.1.183
Maintainer reply None cached
Activity 10 comments · opened Jun 20, 2026

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: makes grep honor .gitignore by default, so a literal grep over 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:

  1. An env var, e.g. CLAUDE_CODE_DISABLE_BUILTIN_SEARCH=1, that skips emitting the find/grep shadow functions into the snapshot.
  2. A settings.json key (e.g. "disableBuiltinSearchShadows": true).
  3. 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/SHADOW toggles — 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.

View original on GitHub ↗

4 Comments

github-actions[bot] · 2 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/60325
  2. https://github.com/anthropics/claude-code/issues/67623
  3. https://github.com/anthropics/claude-code/issues/62642

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

ccharname · 2 months ago

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:

  • bfs parses leading-dash paths differently from GNU find, so find ./-somedir -name '*.x' fails (Unknown argument) where stock find works.
  • ugrep's default --ignore-files makes a literal grep silently honor .gitignore — a different result set with no error.

A disable switch (env var or settings.json key) would be the umbrella mitigation: it makes every shadow-related failure above avoidable for users who just want stock find/grep semantics. Keeping this open as the consolidated opt-out request.

Royyyylin · 1 month ago

+1 for the opt-out. Adding a failure mode of the grep shadow 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):

grep -c x <(printf "x\nx\n")          # shadow/ugrep path -> NO output at all
command grep -c x <(printf "x\nx\n")  # -> 2 (correct)

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 -G override (#59517) and the --ignore-files/-I default 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; bare grep only for interactive human-read output.

jun-cmd · 1 month ago

Two measured consequences of the injected grep shadow (Claude Code 2.1.185, macOS, zsh snapshot), adding to the case for an opt-out:

1. -v exit-code semantics diverge from POSIX/GNU grep.

$ printf 'a\nb\n' | grep -qv a; echo $?
1        # shadow function (embedded ugrep): lines WERE selected, still exits 1
$ printf 'a\nb\n' | /usr/bin/grep -qv a; echo $?
0        # BSD/GNU: exits 0 when any line is selected

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-files silently excludes gitignored files — secret sweeps report false-clean.

$ git init t && cd t && echo 'SECRET=1' > .env && echo '.env' > .gitignore
$ grep -rl SECRET .          # shadow: no matches
$ command grep -rl SECRET .  # ./.env

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.sh don't inherit the function), which is precisely the unguarded ad-hoc layer.

Until an opt-out exists, the workaround we've adopted: command grep for anything judgment-bearing, plus a SessionStart hook that detects ARGV0=ugrep in the newest ~/.claude/shell-snapshots/snapshot-*.sh and warns the model at session start. +1 for CLAUDE_CODE_DISABLE_BUILTIN_SEARCH (or at minimum: don't pass --ignore-files when invoked under the name grep, and match POSIX exit semantics).

Showing cached comments. Read the full discussion on GitHub ↗