claude-harness/hooks/guard-main.sh

45 lines
2.2 KiB
Bash
Executable File

#!/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"*|*"/.claude/harness/adopt-project.sh"*) 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