[BUG] bwrap fails in a worktree when .claude/skills is a symlink in a repository

Status Fixed / completed
Reported on v2.1.92
Maintainer reply None cached
Activity 6 comments · opened Apr 7, 2026 · closed Aug 19, 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?

Hello,
Our repository at my job has the following setup:
.ai-agents/skills
.claude/skills --> ../.ai-agents/skills

This causes Claude to fail with sandbox: true, bwrap installed and launching with --worktree. See error message below.

What Should Happen?

sandbox: true is expected to work with worktrees regardless of repository structure.

Error Messages/Logs

● Bash(pwd && ls .claude/)                                                                   
  ⎿  Error: Exit code 1                                           
     bwrap: Can't create file at /home/claude-aiven-3/git/aiven-core/.claude/skills: Is a    
     directory

Steps to Reproduce

  1. Create a new git repository
  2. mkdir -p .ai-agents/skills
  3. mkdir .claude
  4. Symlink .claude/skills to point to .ai-agents/skills
  5. Launch Claude Code from this workspace with claude --worktree test with sandbox enabled in settings.json
  6. Have the Claude instance execute a bash command

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.92 (Claude Code)

Platform

AWS Bedrock

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

The problem goes away with copy-pasted directories. I would guess that bwrap has a problem when copying over the .claude directory because it doesn't check the subdirectories existing with lstat, although I could be wrong.

View original on GitHub ↗

5 Comments

joylovecorn · 4 months ago

Same error in 2.1.118 / 2.1.119 / 2.1.121, with the entire .claude/ (not just .claude/skills) being the symlink. Workaround: sandbox.enabled: false.

joker-eph · 3 months ago

Here is a bwrap workaround (tested on Ubuntu 22.04 / 24.04) :

#!/bin/bash
# Fixes for:
#   Claude Code assumes deny-path targets are files, but symlinked directories
#      (e.g. .claude/skills -> ../skills) cause bwrap to fail
#      Error: "bwrap: Can't create file at .claude/skills: Is a directory"

set -euo pipefail

need_sudo() {
    if [ "$(id -u)" -ne 0 ]; then
        echo "Requires root. Re-running with sudo..."
        exec sudo "$0" "$@"
    fi
}

need_sudo "$@"

BWRAP_BIN=/usr/bin/bwrap
BWRAP_REAL=/usr/bin/bwrap.real

if [ ! -x "$BWRAP_BIN" ] && [ ! -x "$BWRAP_REAL" ]; then
    echo "[ERROR] bwrap not found at $BWRAP_BIN. Install it first:"
    echo "  sudo apt-get install -y bubblewrap socat"
    exit 1
fi

echo "=== Fix: wrapping bwrap to unwrap the problem ==="

# Back up original binary if not already done
if [ ! -x "$BWRAP_REAL" ]; then
    mv "$BWRAP_BIN" "$BWRAP_REAL"
    echo "  Backed up original to $BWRAP_REAL"
else
    echo "  Backup already exists at $BWRAP_REAL"
fi

cat > "$BWRAP_BIN" << 'WRAPPER'
#!/bin/bash
# bwrap wrapper: fix Claude Code sandbox for symlinked directories and skills access.
# Claude Code does --ro-bind /dev/null <path> to deny access to sensitive paths.
# Two fixes:
#   1. Paths matching */skills or */skills/* — replace with read-only bind mount of
#      the real directory. Agents can read skills; writes still blocked at both the
#      bwrap level and the permission layer.
#   2. Other directories — /dev/null is a file, so bind-mounting it over a directory
#      (or symlink to one) fails. Replace with --tmpfs <path> --remount-ro <path>
#      (empty read-only directory — same deny effect, works on directories).

args=()
i=1
while [ $i -le $# ]; do
    arg="${!i}"
    if [ "$arg" = "--ro-bind" ]; then
        next=$((i + 1))
        dest=$((i + 2))
        src="${!next}"
        dst="${!dest}"
        if [ "$src" = "/dev/null" ]; then
            # Skills directories: read-only bind (not empty tmpfs) so agents can read skills
            if [[ "$dst" == */skills ]] || [[ "$dst" == */skills/* ]]; then
                args+=("--ro-bind" "$dst" "$dst")
                i=$((i + 3))
                continue
            fi
            # Other directories: mount empty read-only tmpfs (same deny, works on dirs)
            if [ -d "$dst" ]; then
                args+=("--tmpfs" "$dst" "--remount-ro" "$dst")
                i=$((i + 3))
                continue
            fi
        fi
    fi
    args+=("$arg")
    i=$((i + 1))
done

exec /usr/bin/bwrap.real "${args[@]}"
WRAPPER

chmod +x "$BWRAP_BIN"
echo "  Installed wrapper at $BWRAP_BIN"
lirundong · 3 months ago

Still reproduces on 2.1.159 (Linux x86_64, bubblewrap). Same layout as the repro: .claude/skills is a symlink to ../.agents/skills (a real directory), used as a single-source-of-truth so skills can be shared across agent tools.

With sandbox.enabled: true, sandbox init aborts for every Bash command (even true), before the command runs:

bwrap: Can't create file at <repo>/.claude/skills: Is a directory

The harness auto-adds .claude/skills to the sandbox deny list and masks it by binding an empty file over it, without lstat-following the symlink. Since it resolves to a directory, bwrap fails with EISDIR. A symlinked file (e.g. a settings.json) masks fine — only symlinked directories break.

The only clean local workaround is sandbox.enabled: false, which also disables autoAllowBashIfSandboxed. A proper fix would lstat the deny path and bind an empty directory when it resolves to a dir/symlinked-dir. Likely the same root cause as #40133.

shawnsw · 2 months ago

Issue still present with version 2.1.177 as of 14/06/2026

Sheraf-DNG · 1 month ago

Still present in version 2.1.199

When a protected path inside the project's .claude/ directory (commands, agents,
rules) is a symlink to a directory, sandbox mount setup fails and every sandboxed Bash
command errors before running:

bwrap: Can't create file at /home/user/repo/.claude/commands: Is a directory

Showing cached comments. Read the full discussion on GitHub ↗