sandbox.enabled: true triggers a synchronous full-tree directory walk on startup → multi-minute hang in large workspaces
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
syscallwithrax=0x101(openat) andflags=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 thesandboxkey removed → fast.permissions/enabledPlugins/ LSP plugins /.git/CLAUDE.md/ Claude version all ruled out by direct test. - The two self-referential Composer
vendorsymlinks 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: falsefor the workspace (loses sandboxing).- Launch from a smaller subdirectory.
- Remove self-referential symlinks from the tree.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗