Sandbox: auto-denied .env paths are not canonicalized, so one symlinked component aborts bwrap and disables all Bash
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 andallowUnsandboxedCommands: 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:
- Resolve discovered paths with
realpathbefore passing them to bwrap. The canonical
path binds correctly, as shown above, so this fixes the case outright while keeping
the deny in force.
- 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
/lib64symlink resolution;
another instance of symlink handling during new-root construction.