sandbox.enabled: true triggers a synchronous full-tree directory walk on startup → multi-minute hang in large workspaces

Open 💬 2 comments Opened Jun 15, 2026 by LauJosefsen

Summary

With sandbox.enabled: true, Claude Code performs a synchronous, single-threaded recursive walk of the entire working-tree directory structure on every prompt, before the first API request. It runs on the JS main thread, does not respect .gitignore/.ignore/.claudeignore, and follows symlinks (re-traversing self-referential loops). Its duration scales with the number of directories. With sandbox.enabled: false, the identical workspace is instant.

Real-world impact: in a ~256k-directory workspace, every prompt hangs ~171s with sandbox on vs ~5s with it off.

Environment

  • Claude Code 2.1.177 (also reproduced on 2.1.165 / 2.1.175 / 2.1.176 — not a version regression)
  • Linux x86-64, native install; bubblewrap 0.11.0

---

Reproducer A — minimal, isolates the symlink-following defect

One self-referential symlink + a few thousand directories. Both ingredients are individually harmless; only the combination hangs.

#!/usr/bin/env bash
set -euo pipefail
K="${K:-6000}"; BIN="${BIN:-claude}"
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
mkdir -p "$T/.claude" "$T/base"; git -C "$T" init -q

# a few thousand ordinary directories
( cd "$T/base"; i=0; while [ "$i" -lt "$K" ]; do mkdir -p "p$((i/50))/s$i"; i=$((i+1)); done )

# ONE absolute, self-referential symlink loop
# (mimics Composer's orchestra/testbench: <pkg>/vendor/.../laravel/vendor -> <pkg>/vendor)
ln -s "$T/base" "$T/base/self_loop"

probe(){ local s=$SECONDS
  ( cd "$T" && timeout 600 "$BIN" -p "say hi" >/dev/null 2>&1 || true )
  printf '  %-26s %ss\n' "$1" "$((SECONDS-s))"; }

printf '{"sandbox":{"enabled":false}}\n' > "$T/.claude/settings.local.json"
probe "loop, sandbox DISABLED"
rm "$T/base/self_loop"
printf '{"sandbox":{"enabled":true,"autoAllowBashIfSandboxed":true}}\n' > "$T/.claude/settings.local.json"
probe "no loop, sandbox ENABLED"
ln -s "$T/base" "$T/base/self_loop"
probe "loop, sandbox ENABLED"

Results (6,121 base directories):

  loop, sandbox DISABLED     5s    <- harmless on its own
  no loop, sandbox ENABLED   7s    <- harmless on its own
  loop, sandbox ENABLED      55s   <- only the combination hangs

Raise K= to make the hang arbitrarily long. Verified to reproduce even under a pristine CLAUDE_CONFIG_DIR (no global settings, hooks, or plugins) — so it is the project sandbox setting, not any global config.

Reproducer B — pure scale (no symlinks), confirms the walk is the whole tree

#!/usr/bin/env bash
set -euo pipefail
N="${N:-100000}"; BIN="${BIN:-claude}"
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
cd "$T"; git init -q; mkdir -p .claude
i=0; while [ "$i" -lt $(( N/500 + 1 )) ]; do mkdir -p "d$i"/sub{1..500}; i=$((i+1)); done
probe(){ local s=$SECONDS; timeout 600 "$BIN" -p "say hi" >/dev/null 2>&1 || true; printf '  %-18s %ss\n' "$1" "$((SECONDS-s))"; }
printf '{"sandbox":{"enabled":false}}\n' > .claude/settings.local.json; probe "sandbox DISABLED"
printf '{"sandbox":{"enabled":true,"autoAllowBashIfSandboxed":true}}\n' > .claude/settings.local.json; probe "sandbox ENABLED"

Results (100,713 directories): sandbox DISABLED 7s vs sandbox ENABLED 21s. The gap grows with directory count (256k dirs → 5s vs 171s in the real workspace).

---

Expected vs actual

  • Expected: enabling the sandbox should not add startup latency proportional to the whole working tree. The filesystem-view setup should be lazy/bounded/off-main-thread, prune .git/vendor/node_modules, honor ignore files, and not follow symlink cycles.
  • Actual: a blocking O(directories) walk on the main thread before every request, ignoring all ignore rules and following symlink loops.

Root-cause evidence

  • gdb sampling of the pegged process: the hot leaf is the instruction immediately after a syscall with rax=0x101 (openat) and flags=0x10000 (O_DIRECTORY) — a tight loop opening directory after directory, single JS caller frame.
  • Single-threaded on the main JS thread (worker/GC threads idle).
  • Isolation: removing the project .claude/settings.local.json → fast; a config of only {"sandbox":{"enabled":true}} → slow; same config with the sandbox key removed → fast. permissions / enabledPlugins / LSP plugins / .git / CLAUDE.md / Claude version all ruled out by direct test.
  • The two self-referential Composer vendor symlinks in the real workspace roughly doubled the time (83s → 171s) via re-traversal; an isolated single loop on a tiny tree is handled fine, so cycle handling exists but is overwhelmed at scale.

Impact

Any large workspace (monorepos, multi-repo meta-checkouts, trees with big vendor//node_modules/) becomes unusable at the root with sandbox enabled — every prompt blocks for minutes.

Workarounds

  • sandbox.enabled: false for the workspace (loses sandboxing).
  • Launch from a smaller subdirectory.
  • Remove self-referential symlinks from the tree.

View original on GitHub ↗

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