Bundled bfs find rewrite holds ~65k DIR fds per call on large monorepos, contributes to ENFILE

Resolved 💬 2 comments Opened May 4, 2026 by tylerhughesgormandd Closed May 7, 2026

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

  1. 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 running find will rapidly hit this.
  2. Cascading misleading errors. Once the FD ceiling is hit, the symptoms surface as the misleading errors documented in #47909 — ENOENT on posix_spawn 'rg', "directory no longer exists" on Bash cwd, etc.
  3. Silent. Users running Claude Code in monorepos have no visibility that find is the trigger — they invoked a normal-looking shell command.
  4. Counterintuitive. Native macOS BSD find and GNU find are depth-first / streaming — they never hold this many DIR fds. The bfs substitution is what makes the cost qualitatively different.

Suggested fixes

In rough priority order:

  1. 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.
  2. Expose an opt-out env var like CLAUDE_CODE_DISABLE_FIND_REWRITE=1 — when set, the shell function should command find "$@" and return.
  3. 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.
  4. 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 find calls 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 N to cap the walk

Related

  • #47909 — same underlying FD exhaustion; this issue identifies one concrete cause

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗