#!/usr/bin/env bash # PreToolUse hook: block file changes on main/master and on detached HEAD. # Covers Edit/Write/MultiEdit directly and Bash commands that look like writes. # Single exception: commits/pushes to main that touch only openspec/ (archive step). set -euo pipefail input=$(cat) eval "$(printf '%s' "$input" | python3 -c ' import sys, json, shlex d = json.load(sys.stdin); ti = d.get("tool_input", {}) or {} print("cwd=" + shlex.quote(d.get("cwd", ""))) print("tool=" + shlex.quote(d.get("tool_name", ""))) print("cmd=" + shlex.quote(ti.get("command", "") or ""))' 2>/dev/null || echo 'cwd=; tool=; cmd=')" [ -n "$cwd" ] || exit 0 git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0 branch=$(git -C "$cwd" branch --show-current 2>/dev/null || true) case "$branch" in main|master|"") ;; *) exit 0 ;; esac label=${branch:-"detached HEAD"} only_openspec() { # $1 = git diff range/args; true if every changed path is under openspec/ local files files=$(git -C "$cwd" diff --name-only $1 2>/dev/null || true) [ -n "$files" ] && ! printf '%s\n' "$files" | grep -vq '^openspec/' } if [ "$tool" = "Bash" ]; then case "$cmd" in *"git worktree"*|*"git checkout -b"*|*"git switch -c"*|*"openspec archive"*) exit 0 ;; esac # archive exception: commit of staged openspec-only changes, push of openspec-only commits if printf '%s' "$cmd" | grep -Eq '\bgit\s+commit\b'; then only_openspec "--cached" && exit 0 fi if printf '%s' "$cmd" | grep -Eq '\bgit\s+push\b'; then only_openspec "origin/main...HEAD" && exit 0 fi if printf '%s' "$cmd" | grep -Eq '(^|[^<])>{1,2}[^&]|\btee\b|\bsed\s+-i|\bmv\b|\bcp\b|\brm\b|\btouch\b|\bmkdir\b|\bgit\s+(add|commit|push|apply|stash\s+pop)\b|\bnpm\s+(i|install|init)\b|\bpip\s+install\b'; then echo "BLOCKED: shell write on $label ($(printf '%s' "$cmd" | head -c 80)). Create an isolated worktree first (superpowers:using-git-worktrees). Only 'openspec archive' commits may land on main directly." >&2 exit 2 fi exit 0 fi echo "BLOCKED: you are on $label. Create an isolated worktree first (superpowers:using-git-worktrees), never edit main directly." >&2 exit 2