[BUG] WSL2: sandbox deny-mask mount points still leak 0-byte files into cwd on every Bash call (2.1.216; re-report of locked #26722)

Status Open
Reported on v2.1.47
Maintainer reply None cached
Activity 0 comments · opened Jul 21, 2026

Preflight Checklist

  • [x] I have searched existing issues — the closest reports are #17087 (closed completed 2026-02-06), #17258 / #25603 (closed as duplicates), and #26722 (WSL2-specific re-report on 2.1.47, auto-closed as duplicate of the already-closed #17087, now locked). The lock notice on #26722 instructs filing a new issue and referencing it — this is that issue. No open issue covers it.
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code (2.1.216)

What's Wrong?

On WSL2, every sandboxed Bash tool call still creates persistent 0-byte read-only files in the working directory — the bwrap mount-point leak described in #26722 remains fully reproducible on 2.1.216, five months and ~170 releases after #17087 was closed as completed.

Files observed leaking (2.1.216, today)

Created in the repo root on the first Bash call of a session, mtime refreshed on every subsequent call:

.bash_profile, .bashrc, .gitconfig, .gitmodules, .idea, .profile, .ripgreprc, .vscode, .zprofile, .zshrc (all 0-byte, mode 444; .idea/.vscode leak as files here)

plus newer deny-list paths that did not exist in the February report, showing the deny list has grown while the leak persists:

.claude/launch.json, .claude/loop.md, .claude/output-styles, .claude/routines

Deny-listed paths that do exist in the repo (e.g. .mcp.json, .claude/commands/, .claude/agents/, tracked .env.example files under a read-deny) leave no artifact — consistent with the root cause: bwrap only needs to create a mount-point file when the deny target is absent, and on WSL2 that file is created on the host filesystem and persists after the namespace exits.

Behavior details

  • Inside a sandboxed call the placeholders are live mounts: rm fails with Device or resource busy. Outside, they are plain 0-byte files — deletable, but re-created by the next sandboxed call, so cleanup cannot stick for the life of a session.
  • Existing files under a read-deny appear inside the sandbox as character-device (/dev/null) masks, so git status/git diff run inside the sandbox report them as modified/unhashable ("unsupported file type") even though the on-disk files are intact. Probably by design, but it compounds the confusion: a session diagnosing the leaked untracked files also sees phantom modifications to tracked files.

Impact

The leaked files pollute git status as untracked root-level entries. Any workflow that treats a dirty tree as a signal (stop hooks, commit gates, lint-staged, scripted git status --short checks) breaks on every turn. Our only stable local workaround was committing the 14 artifact names to .gitignorerm-based cleanup (manual or hook-driven) is undone by the next tool call.

Root cause / suggested fixes

Unchanged from #26722, which diagnosed it in the compiled source: --ro-bind /dev/null <cwd>/<dotfile> against a non-existent target forces bwrap to create the mount point, and on WSL2 (unlike native Linux) that mount-point file lands on the host filesystem. The exit-handler cleanup doesn't help because files are re-created per call and cleanup never runs on SIGKILL/crash. Suggested fixes from that report still apply:

  1. On WSL2, skip deny bind-mounts for paths that don't exist (nothing to protect from reads; the leak is worse than the write-creation risk), or
  2. clean up mount-point artifacts immediately after each bwrap invocation, not only on process exit, or
  3. gate deny mounts on existsSync for the target.

What Should Happen?

No files created in the working directory by the sandbox's own masking machinery.

Environment

  • WSL2, kernel 6.6.87.2-microsoft-standard-WSL2 (same as #26722)
  • bubblewrap 0.9.0
  • Claude Code 2.1.216

View original on GitHub ↗