Bundled bfs find rewrite holds ~65k DIR fds per call on large monorepos, contributes to ENFILE
Summary
Claude Code's shell snapshot rewrites every find invocation to its bundled bfs binary (breadth-first find). On large monorepos this single design choice causes large, deterministic spikes in kern.num_files because bfs opens a directory file descriptor for every directory in its current frontier and does not release them until the walk completes. On the monorepo I tested it on (~65,000 directories), one find call holds 65,532 DIR file descriptors simultaneously.
This is one concrete, reproducible source of the broader FD-exhaustion behavior tracked in #47909.
The shell function
~/.claude/shell-snapshots/snapshot-zsh-*.sh (regenerated each session) installs:
find () {
local _cc_bin="${CLAUDE_CODE_EXECPATH:-}"
[[ -x $_cc_bin ]] || _cc_bin=$(command -v claude 2>/dev/null)
if [[ ! -x $_cc_bin ]]; then
command find "$@"
return
fi
if [[ -n $ZSH_VERSION ]]; then
ARGV0=bfs "$_cc_bin" -regextype findutils-default "$@"
elif ...
elif [[ $BASHPID != $$ ]]; then
exec -a bfs "$_cc_bin" -regextype findutils-default "$@"
else
( exec -a bfs "$_cc_bin" -regextype findutils-default "$@" )
fi
}
So every find call inside the Claude Code Bash tool routes through the bundled claude binary acting as bfs.
Reproduction
Environment: macOS 15.4 (Darwin 25.4.0), zsh, Claude Code 2.1.118, kern.maxfiles=491520, kern.maxfilesperproc=245760.
In a Claude Code session inside any large monorepo (mine has ~65k directories):
find <repo-root> -name SOME_FILENAME
While that runs, in another terminal:
sysctl kern.num_files
lsof | awk 'NR>1 {print $1, $2}' | sort -u | awk '{cnt[$2]++} END {for (k in cnt) print cnt[k], k}' | sort -rn | head
Evidence (lsof snapshot during peak)
PID 38229 bfs -regextype findutils-default /Users/<user>/Projects/<monorepo> -name catalog-tool
65,540 FDs total
65,532 of which are DIR
kern.num_files: 74,450 (baseline ~8,800; Δ +65,650 from this single call)
Top file-type breakdown at peak:
41,960 DIR ← almost entirely from the bfs frontier
10,905 REG
1,492 CHR
174 IPv4
173 unix
25 IPv6
When the walk completes, all 65,532 DIR fds release simultaneously (verified — monitor recorded Δ-65,539 the moment the process exited).
Why this is bad
- System-wide ENFILE.
kern.maxfiles=491,520— only ~7 concurrent walks on a tree this size will exhaust the system file table. Cable-lens,cable, or any flow that fans out parallel Bash subagents each runningfindwill rapidly hit this. - Cascading misleading errors. Once the FD ceiling is hit, the symptoms surface as the misleading errors documented in #47909 —
ENOENTonposix_spawn 'rg', "directory no longer exists" on Bash cwd, etc. - Silent. Users running Claude Code in monorepos have no visibility that
findis the trigger — they invoked a normal-looking shell command. - Counterintuitive. Native macOS BSD
findand GNUfindare depth-first / streaming — they never hold this many DIR fds. Thebfssubstitution is what makes the cost qualitatively different.
Suggested fixes
In rough priority order:
- Cap concurrent open DIR fds in the bundled
bfs. A configurable upper bound on the frontier (e.g. close + reopen LRU descriptors) would prevent unbounded growth on huge trees with little change to user-visible behavior. - Expose an opt-out env var like
CLAUDE_CODE_DISABLE_FIND_REWRITE=1— when set, the shell function shouldcommand find "$@"and return. - Make the rewrite skip large trees. A heuristic check (e.g., if the target path contains > N top-level directories) that falls back to system
find. - Document the rewrite. Currently invisible to users; surprising when it manifests. A note in the shell-snapshot file or in the docs would help users understand why their
findcalls behave differently.
Workarounds (for users hitting this today)
- Use
git ls-files | grep <pattern>for tracked-file searches in repos - Use
rg --files <path> | grep <pattern>for streaming file enumeration - Prefix with
command(command find /path -name X) to bypass the function - Add
-maxdepth Nto cap the walk
Related
- #47909 — same underlying FD exhaustion; this issue identifies one concrete cause
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗