Sandbox: auto-denied .env paths are not canonicalized, so one symlinked component aborts bwrap and disables all Bash

Status Open
Reported on v2.1.231
Maintainer reply None cached
Activity 0 comments · opened Aug 13, 2026

Sandbox: auto-denied .env paths are not canonicalized, so one symlinked component aborts bwrap and disables all Bash

*Paths and file names in this report are anonymized. The directory structure, symlink
layout, and all command output are otherwise unchanged.*

Summary

When the working directory contains a symlink, the startup scan that collects .env
files records the symlink-relative path rather than the resolved real path. bwrap
refuses to create a mount point across a symlinked component, so sandbox setup aborts
before any command runs. With failIfUnavailable: true and
allowUnsandboxedCommands: false, the result is that every Bash call in the session
fails — including commands unrelated to the file in question — and subagents fail the
same way. The session is unusable for anything requiring a shell.

The failure is not diagnosable from inside the session, because diagnosing it requires
running commands.

Environment

  • Claude Code: 2.1.231 (Claude Code)
  • OS: Arch Linux, kernel 6.6.31-1-lts
  • bubblewrap: 0.11.2
  • Working directory: /home/user (i.e. $HOME)
  • Filesystem: single ext4 root on LUKS; no separate mount for the affected subtree

Relevant ~/.claude/settings.json:

"sandbox": {
  "enabled": true,
  "failIfUnavailable": true,
  "autoAllowBashIfSandboxed": true,
  "allowUnsandboxedCommands": false
}

Symptom

Every Bash tool call returns, before the command executes:

bwrap: Can't mkdir parents for /home/user/dev/vendor/example-docs/site-generator-1.0/.env: No such file or directory

The named .env exists and is readable. It is incidental — it is simply the first
entry in the generated deny list whose path crosses a symlink.

Root cause

/home/user/dev is an absolute symlink to /srv/home-store/dev/. The scan follows it
and stores hits under /home/user/dev/... instead of the resolved
/srv/home-store/dev/.... bwrap will not create a mount point through a symlinked path
component — deliberate, since following a symlink while building the new root is an
escape vector — so it aborts.

namei -l on the recorded path:

f: /home/user/dev/vendor/example-docs/site-generator-1.0/.env
 dr-xr-xr-x root root /
 drwxr-xr-x root root home
 drwx------ user user user
 lrwxrwxrwx user user dev -> /srv/home-store/dev/
   dr-xr-xr-x root root   /
   drwxr-xr-x root root   srv
   drwxr-xr-x user user   home-store
   drwxr-xr-x user user   dev
 drwxr-xr-x user user vendor
 drwxr-xr-x user user example-docs
 drwxr-xr-x user user site-generator-1.0
 -rwxr-xr-x user user .env

Minimal reproduction

Independent of Claude Code — plain bwrap, one bind:

mkdir -p /tmp/real/project
touch /tmp/real/project/.env
ln -s /tmp/real /tmp/link

# via the symlink: fails
bwrap --dev-bind / / --bind /dev/null /tmp/link/project/.env true; echo "symlinked=$?"

# resolved path, same file: succeeds
bwrap --dev-bind / / --bind /dev/null /tmp/real/project/.env true; echo "canonical=$?"

Observed on this machine, against the path from the deny list and its resolved
equivalent:

$ bwrap --dev-bind / / --bind /dev/null "/home/user/dev/vendor/example-docs/site-generator-1.0/.env" true
bwrap: Can't mkdir parents for /home/user/dev/vendor/example-docs/site-generator-1.0/.env: No such file or directory
exit=1

$ bwrap --dev-bind / / --bind /dev/null "/srv/home-store/dev/vendor/example-docs/site-generator-1.0/.env" true
canonical=0

Controls confirming file binds work generally on this system:

bwrap --dev-bind / / --bind /dev/null /tmp/probe.env     -> 0
bwrap --dev-bind / / --bind /dev/null /home/user/.bashrc -> 0

To reproduce end to end: place a .env anywhere under a symlinked directory inside the
working directory, then start Claude Code there and run any Bash command.

Expected

Either of:

  1. Resolve discovered paths with realpath before passing them to bwrap. The canonical

path binds correctly, as shown above, so this fixes the case outright while keeping
the deny in force.

  1. Failing that, skip a deny entry whose mount point cannot be created and warn, rather

than aborting sandbox setup. Silently dropping a deny would weaken a security
boundary, so (1) is the correct fix and (2) is only a fallback.

Additionally, the error surfaced to the user names a .env file that exists and is
readable, which points at the wrong thing entirely. Naming the symlinked component
would make this self-diagnosable.

Actual

Sandbox setup aborts. No shell command can run for the lifetime of the session.

Workaround

Start Claude Code from the canonical path (cd "$(readlink -f ~/dev)"/project && claude)
so the scan never crosses the symlink. Deleting the individual .env also unblocks it
until the next .env appears under the symlinked tree.

Related

  • #45451 — same root cause, reported 2026-04-08, closed as not planned by the stale

bot without triage, now locked. Filing fresh as its closing message directs.

  • #79997 — deny mountpoints fail closed instead of skipping missing paths; adjacent

fail-closed behaviour in the same code path.

  • #64799 — bwrap setup broken on merged-usr Arch via /lib64 symlink resolution;

another instance of symlink handling during new-root construction.

View original on GitHub ↗