From 1dea5c697cd21cd09e0106a6df3c95362404f962 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sun, 28 Jun 2026 06:48:37 +0200 Subject: [PATCH] feat: sandboxed claude session launcher scripts Co-Authored-By: Claude Opus 4.8 --- scripts/refactor-claude.sh | 11 ++++ scripts/sandbox-claude.sh | 58 ++++++++++++++++++ scripts/sandbox-settings.json | 107 ++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100755 scripts/refactor-claude.sh create mode 100755 scripts/sandbox-claude.sh create mode 100644 scripts/sandbox-settings.json diff --git a/scripts/refactor-claude.sh b/scripts/refactor-claude.sh new file mode 100755 index 0000000..bef8dc1 --- /dev/null +++ b/scripts/refactor-claude.sh @@ -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 directly. + +set -euo pipefail +exec "$(dirname "$(readlink -f "$0")")/sandbox-claude.sh" "$@" diff --git a/scripts/sandbox-claude.sh b/scripts/sandbox-claude.sh new file mode 100755 index 0000000..8cfc2bc --- /dev/null +++ b/scripts/sandbox-claude.sh @@ -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 # target = +# sandbox-claude.sh ... +# # remaining args pass to claude +# +# Behaviour: +# - cd to +# - copy /sandbox-settings.json -> /.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 /.claude/ refreshes from it. +# +# 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 "$@" diff --git a/scripts/sandbox-settings.json b/scripts/sandbox-settings.json new file mode 100644 index 0000000..f99f5ba --- /dev/null +++ b/scripts/sandbox-settings.json @@ -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" + ] + } +}