sandbox.credentials.files glob expansion costs ~7GB RSS and ~50s on every session start
Summary
A single glob pattern in sandbox.credentials.files makes every session allocate ~7 GB peak RSS and spend ~50 s in startup before the prompt accepts input. The cost is paid on every session start, including -p runs that never invoke a tool.
Because the delay happens before the TUI becomes responsive, an interactive session looks indistinguishable from a hang — that is how I originally encountered this.
Minimal reproduction
A config containing nothing but one deny glob is sufficient:
mkdir -p /tmp/cfgtest
cat > /tmp/cfgtest/settings.json <<'EOF'
{"sandbox":{"enabled":true,"credentials":{"files":[{"path":"~/**/.env","mode":"deny"}]}}}
EOF
ln -s ~/.claude/.credentials.json /tmp/cfgtest/.credentials.json
cd "$(mktemp -d)"
CLAUDE_CONFIG_DIR=/tmp/cfgtest claude -p 'Reply with exactly: hello. Do not use any tools.'
Sample peak RSS via VmHWM in /proc/<pid>/status while it runs.
Measurements
Each pattern was the sole entry in an otherwise blank config, same prompt, same machine ($HOME contains ~284k files):
| deny entry | wall | peak RSS |
|---|---|---|
| (blank config — no sandbox entry) | 3 s | 271 MB |
| literal path to a single file | 8 s | 290 MB |
| ~/code/*/.env | 17 s | 2332 MB |
| ~/code/*/*/.env | 23 s | 2315 MB |
| ~/**/.env | 55 s | 7095 MB |
For comparison, find "$HOME" -name .env performs the same traversal in 185 ms and returns 1 match. Claude Code is roughly 300× slower and allocates ~7 GB doing it.
Note that a single-level * glob still costs 2.3 GB and 17 s. ~/code/*/.env can only match a handful of directories, so this is not simply "** walks a lot of files" — glob expansion in this code path appears to be superlinear in the tree it scans, independent of how many paths actually match.
Bisection
The cost is entirely attributable to the deny glob, not to other config surface:
| config | wall | peak RSS |
|---|---|---|
| blank | 3 s | 271 MB |
| + settings.json (full, real) | 50 s | 7105 MB |
| + CLAUDE.md (26 KB) | 5 s | 266 MB |
| + skills/ (216 files) | 4 s | 273 MB |
| + plugins/ (501 files) | 3 s | 275 MB |
And within settings.json:
| section only | wall | peak RSS |
|---|---|---|
| hooks (6 events) | 7 s | 281 MB |
| permissions.deny (7 entries) | 6 s | 276 MB |
| sandbox.credentials.envVars (19) | 8 s | 282 MB |
| sandbox.credentials.files (10, incl. glob) | 65 s | 7125 MB |
Removing the one glob entry took real sessions from 52 s / 7132 MB to 4 s / 286 MB.
Memory is transient rather than leaked — steady-state RSS is ~500 MB, with the peak occurring during startup.
Expected
Expanding a handful of deny globs should cost milliseconds and negligible memory, comparable to find.
Actual
~7 GB peak RSS and ~50 s added to every session start.
Environment
- Claude Code 2.1.276 (reproduced with both the npm-distributed binary and a Nix-packaged build of the same version — byte-identical Bun 1.4.3 / JavaScriptCore runtime)
- Linux aarch64, 16 KiB page size, 96 GB RAM
$HOME≈ 284k files
Not page-size specific as far as I can tell: with a blank config the same machine starts in 3 s / 271 MB.
Notes
--max-old-space-size, BUN_JSC_forceRAMSize and BUN_JSC_gcMaxHeapSize all had no effect on the peak, which suggests the allocation is not bounded by the managed JS heap.
Suggested workaround for other users
Use literal paths in sandbox.credentials.files; avoid glob patterns entirely until this is addressed.