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"
+ ]
+ }
+}