[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)
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:
rmfails withDevice 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, sogit status/git diffrun 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 .gitignore — rm-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:
- 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
- clean up mount-point artifacts immediately after each bwrap invocation, not only on process exit, or
- gate deny mounts on
existsSyncfor 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