[BUG] Sandbox recursively enumerates workspace into nested node_modules → unbounded memory → OOM (still repros on 2.1.195; cf #27863)

Resolved 💬 3 comments Opened Jun 29, 2026 by zordius Closed Jun 29, 2026

Sandbox mode (sandbox.enabled: true) makes claude recursively enumerate the workspace on the first conversation turn and descend into deeply-nested node_modules, growing RSS without bound until the kernel OOM-kills it.

Previously reported in #27863 (auto-closed as stale / not planned) — still reproduces on 2.1.195, and I traced the exact mechanism with strace.

Environment

  • Claude Code 2.1.195, Ubuntu 24.04, 32 GB RAM, swap=0
  • sandbox.enabled: true
  • cwd contains a large JS monorepo with deeply-nested node_modules (tens of GB of directory entries)

Repro

  1. sandbox.enabled: true, cwd = a repo with a big nested node_modules.
  2. Start claude, send any trivial message (e.g. test).
  3. The main claude process spins (R) and RSS climbs ~2–2.5 GB/min: ~290 MB → 3 GB → 5 GB → 7.75 GB … → OOM. With swap=0 the OOM killer takes sshd and the box becomes unreachable (matches the "system freeze / no errors in debug logs" reports).

Root cause (strace) — pure directory-metadata walk, no file contents read

strace -f -c -p <pid>   (3s):
 % time   calls   syscall
  12.12   20240   getdents64
   8.04   10106   openat
   4.98   10106   close
 (47% futex / 26% epoll_pwait2 = the spinning event loop)

/proc/<pid>/io over the same window:  read_bytes: 0   (rchar advanced ~8 bytes)

Sample openat targets (genericized) — note the node_modules-inside-node_modules recursion:

openat(AT, "<repo>/node_modules/@scope-a/<pkg>/node_modules/@scope-b/<pkg>/node_modules/@scope-c/<pkg>/node_modules/<pkg>", O_RDONLY|O_DIRECTORY)
...  (~5,800 such node_modules openat calls per 2-second sample)

This happens before any Bash/tool callbwrap isn't even spawned yet — so it is claude's own pre-tool workspace enumeration, not the sandbox runtime.

Does NOT fix it (so it's not a misconfig — all tried on 2.1.195)

  • Removing all glob patterns from sandbox.filesystem.denyRead → still walks node_modules.
  • sandbox.allowManagedReadPathsOnly: true → still walks (RSS already 4 GB; ~5,800 node_modules openat/2s).
  • .claudeignore containing node_modules/ → still walks (strace: 5,437 node_modules openat in a 2s sample). The enumeration ignores .claudeignore entirely — directly confirming the "even with ignore files configured" note in #27863.

Avoids it

  • sandbox.enabled: false → no walk, no balloon (confirms it is sandbox-specific).
  • Moving the large subtree out of cwd, or running from a small dir → fine.

Recovery

The process ignores SIGTERM (it's spinning); only SIGKILL frees the memory.

Multi-agent amplifies it

Each sub-agent enumerates, and the orchestrator respawns a fresh leaking sub-agent every time one is killed (whack-a-mole) — only killing the whole process tree stops it. Likely the same surface as #34568.

Suggested fix

Don't eagerly enumerate the workspace. A single recursive --ro-bind <workspace> expresses "this directory and below" in O(1); deny-paths can be overlaid individually. At minimum, prune node_modules and honor .gitignore / .claudeignore during whatever walk it does.

View original on GitHub ↗

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