[BUG] Sandbox bwrap fails when ~/.nix-profile exists as a symlink to a directory
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
What's Wrong?
Claude Code's sandbox (bwrap) refuses to start whenever ~/.nix-profile exists as a symlink to a directory — which is the default state on any machine with Nix installed in single-user mode.
Captured the actual bwrap argv with a shim wrapper. Claude Code's sandbox profile masks ~/.nix-profile alongside other "risky" host paths (~/scripts, ~/.github, ~/runners, ~/actions-runner, ~/secrets, ~/.mcp.json, ~/.idea) by binding /dev/null over them:
--ro-bind /dev/null /home/eyad/scripts
--ro-bind /dev/null /home/eyad/.github
--ro-bind /dev/null /home/eyad/runners
--ro-bind /dev/null /home/eyad/actions-runner
--ro-bind /dev/null /home/eyad/.nix-profile ← fails here
--ro-bind /dev/null /home/eyad/secrets
--ro-bind /dev/null /home/eyad/.mcp.json
--ro-bind /dev/null /home/eyad/.idea
This works for the others (regular files or non-existent paths), but fails for .nix-profile because:
/dev/nullis a character device (file-shaped from bwrap's perspective).~/.nix-profileresolves through symlinks to a directory (/nix/store/<hash>-profile/containingbin/,share/,manifest.json).- bwrap can't bind a file over a directory, and emits a misleading ENOENT-style message instead of a type-mismatch error.
A related variant of the same bug: when ~/.nix-profile is a dangling symlink (e.g. after a partial Nix uninstall), the same bind fails for the obvious reason that the resolved path doesn't exist. In both cases the failure is fatal — the entire sandbox refuses to start, blocking every Bash invocation. dangerouslyDisableSandbox: true on the individual tool call doesn't bypass it; bwrap is still invoked and still hits this bind.
What Should Happen?
The sandbox should start successfully on machines with Nix installed. The mask for ~/.nix-profile should work whether the path is a file, a directory, a working symlink to either, or a dangling symlink — and a single broken/mismatched mask entry shouldn't take down the entire sandbox.
Error Messages/Logs
bwrap: Can't create file at /home/eyad/.nix-profile: No such file or directory
Exit code 1, emitted before any user command runs.
Steps to Reproduce
- On Linux, install Nix in single-user mode (creates
~/.nix-profileas a symlink to a directory under/nix/store/). - Enable the sandbox in
~/.claude/settings.json:
``json``
"sandbox": { "enabled": true }
- Start Claude Code and trigger any Bash tool call (
echo hiis enough). - Observe the bwrap error above and immediate exit code 1.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.119 (Claude Code)
Platform
Other
Operating System
Other Linux
OS: Arch Linux (Omarchy) / Linux 6.19.13-arch1-1
Terminal/Shell
- Shell: fish (interactive) / bash (Claude Code subprocesses)
- Kitty
Additional Information
- bwrap:
/usr/bin/bwrap(system package) - Nix: single-user install, store at
/nix/store/, profile at~/.local/state/nix/profiles/profile
Suggested fix
Either:
- Use
--tmpfsfor directory-shaped masks instead of--ro-bind /dev/null. This matches how.aws,.azure,.gnupg,.config/gcloudare already handled in the same profile, and works regardless of whether the destination is a file, a directory, or a symlink to either. - Or, detect the destination type at sandbox-setup time (
stat()with symlink resolution) and pick the appropriate mask flavor (/dev/nullfor files,--tmpfsfor directories).
Bonus: switch fatal binds in this mask block to --ro-bind-try so a single missing/broken host path doesn't take down the whole sandbox.
Workaround
rm ~/.nix-profile
Breaks Nix tooling but unblocks the sandbox by letting bwrap create a fresh empty file at the mount point. Keeping Nix functional while the sandbox is enabled doesn't appear to be possible without modifying the (non-user-configurable) mask list.
How the argv was captured
Wrapped /usr/bin/bwrap with a shim earlier on PATH that logged "$@" to a file before exec-ing the real binary. The full argv showed --ro-bind /dev/null /home/eyad/.nix-profile as one of the mask entries. Happy to share the full argv on request.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗