Created
August 25, 2026 09:05
-
-
Save timkuijsten/5fcfa8937b4cf8916fde6f01a2c2473c to your computer and use it in GitHub Desktop.
claude sandbox (save in ~/.local/bin)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # Ergonomic sandbox for Claude Code CLI using pasta(1) and bwrap(1). It isolates | |
| # your homedir, the localhost network and environment from claude. Export | |
| # CLAUDE_GH_TOKEN with a restricted fine-grained personal access token if you | |
| # want to give it access to GitHub. | |
| # | |
| # v1.0 2026-07-07 | |
| # v1.1 2026-07-08 add set -u and use init function | |
| # v1.2 2026-07-09 better localhost isolation + tweaks | |
| # v1.3 2026-07-09 test ancestors of home dir | |
| # v1.4 2026-07-23 isolate host session keyring | |
| # v1.5 2026-08-06 drop --new-session only when dev.tty.legacy_tiocsti=0 | |
| set -u | |
| cwd=$(pwd -P) | |
| home=$(cd -P -- "$HOME" && pwd -P) || { | |
| echo "\$HOME not a directory: $HOME" >&2 | |
| exit 1 | |
| } | |
| # Host dirs with user-created files that the sandbox must keep hidden. | |
| # Refuse when $cwd is one of these dirs or an ancestor of one: binding | |
| # $cwd into the sandbox would then expose the protected files. | |
| # ($cwd is an ancestor-or-self of $p exactly when $p/ starts with $cwd/, | |
| # hence $p is the case subject and $cwd the pattern.) | |
| for p in "$home" /tmp /var/tmp /dev/shm "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" | |
| do | |
| case ${p%/}/ in | |
| "${cwd%/}"/*) | |
| echo "refusing: $cwd is or contains $p, running here would expose it" >&2 | |
| exit 1 ;; | |
| esac | |
| done | |
| command -v pasta >/dev/null || { | |
| echo "pasta missing: dnf install passt" >&2 | |
| exit 1 | |
| } | |
| # --new-session blocks TIOCSTI terminal injection from inside the sandbox | |
| # at the cost of breaking window resize. Only drop it when the kernel | |
| # already enforces the same protection via dev.tty.legacy_tiocsti=0; | |
| # otherwise fall back to --new-session. | |
| tiocsti=/proc/sys/dev/tty/legacy_tiocsti | |
| new_session_flag="--new-session" | |
| val=$(cat "$tiocsti" 2>/dev/null) && [ "$val" = 0 ] && new_session_flag="" | |
| # If this is the first time Claude Code CLI is started, enable claude's own | |
| # sandbox as well, so that auto-allow is enabled for a lot of commands improving | |
| # ergonomics. Note: Claude can disable its own sandbox, that's why we sandbox it | |
| # ourselves anyway. | |
| init() { | |
| if [ ! -e "$HOME/.claude.json" ]; then | |
| # claude's internal sandbox proxy needs socat | |
| command -v socat >/dev/null || { | |
| echo "socat missing: dnf install socat" >&2 | |
| exit 1 | |
| } | |
| echo '{}' > ~/.claude.json && chmod 600 ~/.claude.json | |
| fi | |
| if [ ! -d "$HOME/.claude" ]; then | |
| mkdir -m700 ~/.claude && cat > ~/.claude/settings.json <<-eof | |
| { | |
| "sandbox": { | |
| "enabled": true, | |
| "failIfUnavailable": true, | |
| "allowUnsandboxedCommands": false | |
| } | |
| } | |
| eof | |
| fi | |
| } | |
| init | |
| # carry-over any CLAUDE_GH_TOKEN | |
| # TODO use file instead of env and command-line | |
| gh_flag="" | |
| [ -n "${CLAUDE_GH_TOKEN:-}" ] && gh_flag="--setenv GH_TOKEN $CLAUDE_GH_TOKEN" | |
| # We give it its own network to shield it from localhost, therefore we | |
| # can't --unshare-all but have to unshare everything except --unshare-net. | |
| # | |
| # Each invocation gives the sandbox a fresh session keyring instead of the | |
| # caller's; --unshare-user separately isolates user and persistent keyrings. | |
| # These keyrings are not inherited by separately launched sandboxes and are | |
| # discarded after the sandbox's last process exits. | |
| exec keyctl session - \ | |
| pasta \ | |
| --config-net \ | |
| --no-map-gw \ | |
| --dns none \ | |
| --tcp-ns none \ | |
| --udp-ns none \ | |
| --tcp-ports none \ | |
| --udp-ports none \ | |
| -- \ | |
| bwrap \ | |
| --ro-bind /usr /usr \ | |
| --ro-bind /etc /etc \ | |
| --symlink usr/lib /lib \ | |
| --symlink usr/lib64 /lib64 \ | |
| --symlink usr/bin /bin \ | |
| --symlink usr/sbin /sbin \ | |
| --proc /proc \ | |
| --dev /dev \ | |
| --tmpfs /tmp \ | |
| --tmpfs /run \ | |
| --tmpfs "$HOME" \ | |
| --unshare-user --unshare-ipc --unshare-pid --unshare-uts --unshare-cgroup \ | |
| --die-with-parent \ | |
| $new_session_flag \ | |
| --clearenv \ | |
| --setenv PATH "$PATH" \ | |
| --setenv HOME "$HOME" \ | |
| --setenv USER "${USER:-$(id -un)}" \ | |
| --setenv TERM "${TERM:-vt100}" \ | |
| --setenv LANG "${LANG:-C.UTF-8}" \ | |
| \ | |
| $gh_flag \ | |
| --ro-bind /run/systemd/resolve/resolv.conf /run/systemd/resolve/stub-resolv.conf \ | |
| --bind ~/.claude ~/.claude \ | |
| --bind ~/.claude.json ~/.claude.json \ | |
| --bind "$PWD" "$PWD" \ | |
| --chdir "$PWD" \ | |
| -- claude "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment