#!/usr/bin/env bash # Run Claude Code inside a bubblewrap sandbox. # Filesystem: read-only except for the project dir and /tmp # Network: shared with host (needed to reach docker-compose services on localhost) # # Prerequisite: bwrap must have the suid bit set to run unprivileged: # sudo chmod u+s $(which bwrap) # # Usage: sandbox.sh [-o|--opencode] [-s|--shell] [args...] # -o, --opencode launch opencode instead of claude (default) # -s, --shell launch an interactive bash shell for inspection set -euo pipefail LAUNCH_OPENCODE=0 LAUNCH_SHELL=0 while [ $# -gt 0 ]; do case "$1" in -o|--opencode) LAUNCH_OPENCODE=1; shift ;; -s|--shell) LAUNCH_SHELL=1; shift ;; *) break ;; esac done # Rename the tmux window to the project dir so it's easy to identify. if [ -n "$TMUX_PANE" ]; then tmux rename-window "$(basename "$PWD")" fi if [ "$LAUNCH_SHELL" -eq 1 ]; then COMMAND=(bash --noprofile --norc) elif [ "$LAUNCH_OPENCODE" -eq 1 ]; then COMMAND=("$HOME/.opencode/bin/opencode") else COMMAND=("$HOME/.local/bin/claude") fi exec bwrap \ --ro-bind /usr /usr \ --symlink usr/bin /bin \ --symlink usr/sbin /sbin \ --symlink usr/lib /lib \ --symlink usr/lib64 /lib64 \ --ro-bind /etc/resolv.conf /etc/resolv.conf \ --ro-bind /etc/hosts /etc/hosts \ --ro-bind /etc/passwd /etc/passwd \ --ro-bind /etc/group /etc/group \ --ro-bind /etc/nsswitch.conf /etc/nsswitch.conf \ --ro-bind /etc/localtime /etc/localtime \ --ro-bind /etc/ssl/certs /etc/ssl/certs \ --dev /dev \ --proc /proc \ --tmpfs /tmp \ --tmpfs /home \ --bind "$PWD" "$PWD" \ --ro-bind "$HOME/.local" "$HOME/.local" \ --ro-bind "$HOME/.nvm" "$HOME/.nvm" \ --bind "$HOME/.claude" "$HOME/.claude" \ --bind "$HOME/.claude.json" "$HOME/.claude.json" \ --bind "$HOME/.opencode" "$HOME/.opencode" \ --unshare-pid \ --die-with-parent \ --chdir "$PWD" \ -- "${COMMAND[@]}" "$@"