[BUG] Sandbox bwrap fails when ~/.nix-profile exists as a symlink to a directory

Resolved 💬 2 comments Opened Apr 26, 2026 by eyadsibai Closed May 28, 2026

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/null is a character device (file-shaped from bwrap's perspective).
  • ~/.nix-profile resolves through symlinks to a directory (/nix/store/<hash>-profile/ containing bin/, 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

  1. On Linux, install Nix in single-user mode (creates ~/.nix-profile as a symlink to a directory under /nix/store/).
  2. Enable the sandbox in ~/.claude/settings.json:

``json
"sandbox": { "enabled": true }
``

  1. Start Claude Code and trigger any Bash tool call (echo hi is enough).
  2. 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 --tmpfs for directory-shaped masks instead of --ro-bind /dev/null. This matches how .aws, .azure, .gnupg, .config/gcloud are 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/null for files, --tmpfs for 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.

View original on GitHub ↗

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