sandbox.enabled: true causes unbounded memory growth at startup, OOM (2.1.233-2.1.235, WSL2)
sandbox.enabled: true causes unbounded memory growth at startup on WSL2 (2.1.233–2.1.235)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.235)
Possibly related but distinct: the long-running-session leak reports (#84960, #4953,
#79815, #80522) describe gradual growth during active use with no identified trigger.
This one is immediate, at startup, before any output, and has a single-key deterministic
reproduction — so it may be a different fault, or the same one with a trigger finally
pinned down.
Summary
Setting "sandbox": {"enabled": true} in settings causes the CLI to enter an unbounded
memory allocation loop at startup. The process never produces output, never reaches the
prompt, and grows until it is killed or the kernel OOM-kills it. Peak observed in real
use: 12.9 GB RSS, which OOM-killed a 16 GB VM.
enabled: false and omitting the key entirely are both clean. No other sandbox sub-key
is involved.
Minimal reproduction
{ "sandbox": { "enabled": true } }
claude -p 'Use the Bash tool to run exactly: echo probe-ok' --allowedTools Bash --settings rung.json
Reproduces 100% of the time here (4/4 runs), version 2.1.235.
To reproduce safely — this will eat all available RAM otherwise:
systemd-run --user --scope -p MemoryMax=2G -p MemorySwapMax=0 \
env DISABLE_AUTOUPDATER=1 \
timeout 150 claude -p '...' --allowedTools Bash --settings rung.json < /dev/null
Bisect
Each rung is its own capped run, RSS sampled from /proc/<pid>/status once per second.
| Rung | sandbox block | Outcome |
|---|---|---|
| control | (key absent) | clean — peak 455 MB, 16 s, printed probe-ok |
| f | {"enabled": false} | clean — peak 457 MB, 10 s, printed probe-ok |
| a | {"enabled": true} | OOM-killed — 7 MB → 2.26 GB, ~100 s, zero bytes of output |
| b | + "failIfUnavailable": false | OOM-killed, same shape |
| c | + "autoAllowBashIfSandboxed": false | OOM-killed, same shape |
| d | + "filesystem": {"denyRead": [<1 path>]} | OOM-killed, same shape |
| e | + "filesystem": {"denyRead": [<3 paths>]} | OOM-killed, 2.26 GB / 98 s |
The single key enabled: true is sufficient. filesystem.denyRead is not involved.
Signature
- Growth: monotonic across all 94 samples, no decreases. ~50 MB/s over the first
30 s, decelerating to ~10 MB/s by 90 s. In the original real-world incidents (larger
memory ceiling, interactive sessions) it ran at 40–90 MB/s.
- Starts immediately: RSS is already climbing at the first 1 s sample, before any
output could exist.
- Zero output: the ballooning process writes 0 bytes. It never reaches the prompt, so
no Bash tool call ever occurs — this is a startup-path fault, not sandbox activation.
- CPU: a single thread hot at 40–85%, process state
R; other threads idle. - Zero I/O during growth.
/proc/<pid>/iocounters were byte-identical across a 60 s
window in which RSS grew ~4 GB — not one syscall, read, or write. Rules out file reads,
subprocess output buffering, and network input.
- Pure anonymous heap:
Private_Dirty==Pss_Anon== 6.7 GB,Pss_File230 MB,
VmSwap 0. No child processes.
- Geometric allocation ladder — largest anonymous mappings from one incident:
869928 kB, 938152 kB, 994976 kB, 1048484 kB, 1048524 kB, 2152516 kB — consistent with
a buffer repeatedly grown and copied, superseded arenas not yet released.
- No transcript is written — affected sessions die before flushing anything, so there
is no in-app record of them.
Environment
| | |
|---|---|
| Versions affected | 2.1.233, 2.1.234, 2.1.235 (all tested — not a single-version regression) |
| Install method | native (~/.local/share/claude/versions/, symlinked into ~/.local/bin) |
| Runtime | Bun (thread names Bun Pool 0..11, mi-scavenger — mimalloc) |
| OS | WSL2 Ubuntu 24.04, kernel 6.18.33.1-microsoft-standard |
| WSL | 2.7.8.0 |
| Windows | 11, build 10.0.26200.9168 |
| Terminal | Windows Terminal 1.24.11911.0 via ConPTY |
| bubblewrap | 0.9.0 at /usr/bin/bwrap — verified healthy: a full --unshare-all namespace setup completes in 8 ms |
Originally surfaced with a sandbox block that also set failIfUnavailable: false,autoAllowBashIfSandboxed: false, and three filesystem.denyRead paths; the bisect above
narrowed it to enabled alone.
Ruled out
- A 2.1.235 regression. Initially compelling (2/2 launches of .235 hung vs 0/137
sessions on .233), refuted when .233 ballooned identically once it read the new config.
- Working directory / project size. Occurred in both a large tree and a small one.
- Paste handling, content, terminal geometry. 15 controlled pty-driven runs bisecting
paste size (4000/4095/4200/5129/8000 B), the 4096-byte pty delivery boundary, terminal
width (100/40/80/0), winsize 0×0, and content class — all negative.
- Hooks. A
statusLinehook was configured; it is apython3subprocess, and zero
syscalls during growth proves it never ran.
- **Shell/bracketed-paste config, tmux, locale, Docker, MCP servers, file watchers,
transcript size.** All inspected; nothing relevant. No containers, no MCP servers
configured, 3 inotify watches total.
Stated limit of the bisect
Every rung above ran in print mode (-p) with --settings. The interactive TTY path
was not re-tested under a memory cap. In the original investigation, 15 controlled
interactive runs with the block present did not balloon — though those runs never
submitted a prompt, and one real interactive incident that also never submitted a prompt
did balloon. That discrepancy is unexplained for the interactive path. What is established:
in print mode, on this machine, sandbox.enabled: true alone reproduces 100% of the time,
and both controls are clean.
Workaround
Remove the sandbox block, or set enabled: false. Effective immediately for new
sessions — RSS flat at 555 MB across 8 consecutive samples afterward.
Secondary note: autoUpdates: false is not honoured (filed separately as #88030)
"autoUpdates": false in the user config did not prevent an update. Pinning a version by
repointing the claude symlink was silently undone at the next launch:
{"timestamp":"...","path":"native","outcome":"success","version_from":"2.1.233","version_to":"2.1.235"}
Only DISABLE_AUTOUPDATER=1 actually holds a version pin. Noted here only because it
directly affects reproducing the bug above — without that env var, a bisect changes
binaries underneath itself. Filed as its own report: #88030.