feat: sandboxed claude session launcher scripts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-28 06:48:37 +02:00
commit 1dea5c697c
3 changed files with 176 additions and 0 deletions

11
scripts/refactor-claude.sh Executable file
View file

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Backwards-compatible wrapper for the ~/nixos-refactor/ worktree workflow.
# Equivalent to `scripts/sandbox-claude.sh` with no target arg — sandboxes
# the worktree containing this script under the policy in
# scripts/sandbox-settings.json.
#
# For sandboxing a different repo (e.g. a new public template under
# development), use sandbox-claude.sh <dir> directly.
set -euo pipefail
exec "$(dirname "$(readlink -f "$0")")/sandbox-claude.sh" "$@"

58
scripts/sandbox-claude.sh Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Launch claude under the sandbox allowlist+deny policy in a target dir.
#
# Usage:
# sandbox-claude.sh # target = parent of script dir
# # (the worktree containing the script)
# sandbox-claude.sh <dir> # target = <dir>
# sandbox-claude.sh <dir> <claude-args>...
# # remaining args pass to claude
#
# Behaviour:
# - cd to <target>
# - copy <script-dir>/sandbox-settings.json -> <target>/.claude/settings.json
# fresh on every launch (idempotent — policy evolves in main and
# propagates on next launch; hand-editing the runtime copy is pointless).
# - exec claude
#
# Safety model (from sandbox-settings.json):
# - defaultMode=acceptEdits — file edits silent
# - allow: narrow Bash set — nix build/check/eval, fmt/lint,
# in-worktree git, basic inspection
# - ask: rm/mv/cp/chmod/chown — prompt every time
# - deny: sudo, *-rebuild switch|test|boot, omni-rebuild, nh, git push,
# git remote, curl/wget/ssh, systemctl, docker/podman, WebFetch,
# WebSearch — hard reject, cannot be overridden
# mid-session
#
# To loosen the policy, edit sandbox-settings.json in the source checkout
# and relaunch — the runtime copy in <target>/.claude/ refreshes from it.
#
# <target> should have `.claude/` in its .gitignore so the runtime copy
# doesn't leak into commits. The script warns if it doesn't.
set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
SETTINGS_SRC="$SCRIPT_DIR/sandbox-settings.json"
TARGET="$SCRIPT_DIR/.."
if [[ $# -gt 0 && -d "$1" ]]; then
TARGET="$1"
shift
fi
TARGET="$(readlink -f "$TARGET")"
[[ -d "$TARGET" ]] || { echo "sandbox-claude: target does not exist: $TARGET" >&2; exit 1; }
[[ -f "$SETTINGS_SRC" ]] || { echo "sandbox-claude: settings file not found: $SETTINGS_SRC" >&2; exit 1; }
cd "$TARGET"
mkdir -p .claude
cp -f "$SETTINGS_SRC" .claude/settings.json
if [[ -f .gitignore ]] && ! grep -qE '(^|/)\.claude/?$' .gitignore; then
echo "sandbox-claude: note — '.claude/' not found in $TARGET/.gitignore;" >&2
echo " the runtime settings.json may leak into commits." >&2
fi
exec claude "$@"

View file

@ -0,0 +1,107 @@
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read",
"Glob",
"Grep",
"Edit",
"Write",
"NotebookEdit",
"Bash(nix build:*)",
"Bash(nix flake check)",
"Bash(nix flake check:*)",
"Bash(nix flake show)",
"Bash(nix flake show:*)",
"Bash(nix flake info)",
"Bash(nix flake info:*)",
"Bash(nix flake metadata)",
"Bash(nix flake metadata:*)",
"Bash(nix eval:*)",
"Bash(nix log:*)",
"Bash(nix-instantiate:*)",
"Bash(nixos-rebuild build:*)",
"Bash(nixos-rebuild dry-build:*)",
"Bash(nixos-rebuild build-vm:*)",
"Bash(nixfmt:*)",
"Bash(statix:*)",
"Bash(deadnix:*)",
"Bash(nix run nixpkgs#nixfmt:*)",
"Bash(nix run nixpkgs#nixfmt-rfc-style:*)",
"Bash(nix run nixpkgs#statix:*)",
"Bash(nix run nixpkgs#deadnix:*)",
"Bash(find:*)",
"Bash(xargs:*)",
"Bash(git status)",
"Bash(git status:*)",
"Bash(git diff)",
"Bash(git diff:*)",
"Bash(git log)",
"Bash(git log:*)",
"Bash(git show)",
"Bash(git show:*)",
"Bash(git branch)",
"Bash(git branch:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(git restore:*)",
"Bash(git stash)",
"Bash(git stash:*)",
"Bash(git rev-parse:*)",
"Bash(git worktree list)",
"Bash(ls:*)",
"Bash(pwd)",
"Bash(which:*)",
"Bash(type:*)",
"Bash(env)",
"Bash(echo:*)",
"Bash(file:*)",
"Bash(stat:*)",
"Bash(wc:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(cat:*)"
],
"ask": [
"Bash(rm:*)",
"Bash(rmdir:*)",
"Bash(mv:*)",
"Bash(cp:*)",
"Bash(chmod:*)",
"Bash(chown:*)"
],
"deny": [
"Bash(sudo:*)",
"Bash(sudo)",
"Bash(doas:*)",
"Bash(su:*)",
"Bash(nixos-rebuild switch:*)",
"Bash(nixos-rebuild test:*)",
"Bash(nixos-rebuild boot:*)",
"Bash(omni-rebuild)",
"Bash(omni-rebuild:*)",
"Bash(nh:*)",
"Bash(git push)",
"Bash(git push:*)",
"Bash(git remote:*)",
"Bash(git reset --hard:*)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Bash(rsync:*)",
"Bash(nc:*)",
"Bash(systemctl:*)",
"Bash(docker:*)",
"Bash(podman:*)",
"WebFetch",
"WebSearch"
]
}
}