Skip to content

Instantly share code, notes, and snippets.

@timkuijsten
Created August 25, 2026 09:05
Show Gist options
  • Select an option

  • Save timkuijsten/5fcfa8937b4cf8916fde6f01a2c2473c to your computer and use it in GitHub Desktop.

Select an option

Save timkuijsten/5fcfa8937b4cf8916fde6f01a2c2473c to your computer and use it in GitHub Desktop.
claude sandbox (save in ~/.local/bin)
#!/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