diff --git a/.claude/hooks/guard-main-checkout.selftest.sh b/.claude/hooks/guard-main-checkout.selftest.sh new file mode 100755 index 0000000000..60d985eebe --- /dev/null +++ b/.claude/hooks/guard-main-checkout.selftest.sh @@ -0,0 +1,320 @@ +#!/usr/bin/env bash +# Self-test for guard-main-checkout.sh — run it after touching that hook: +# +# .claude/hooks/guard-main-checkout.selftest.sh +# +# Feeds the hook the same JSON payload shape Claude Code delivers on PreToolUse and asserts +# the block/allow verdict per case. Hermetic: it builds its OWN throwaway git repos, a linked +# worktree of each and a non-repo directory under $TMPDIR, so the matrix never depends on +# which machine or which checkout it runs from. Needs jq and git and nothing else — no +# install, no build, no network. Exit 0 = all cases hold. +# +# Companion to guard-main-checkout-bash.selftest.sh, which covers the Bash half of the same +# worktree-first pair. That matrix is mostly SHELL SPLITTING; this hook parses no shell at +# all — it reads .tool_input.file_path and makes a PATH-AND-WORKTREE decision — so these +# cases are derived from what this hook actually decides, not ported from the sibling. +# +# Fail-open by default, on purpose: the process cwd AND CLAUDE_PROJECT_DIR both default to a +# directory in no repo at all, which is the input on which this hook allows everything. A +# case that expects `block` therefore cannot pass by accident — the verdict can only have +# come from the path in the payload. Sections that need a different default say so. +# +# GUARD_MAIN_CHECKOUT_HOOK points the matrix at a scratch copy of the hook, and +# GUARD_MAIN_CHECKOUT_SETTINGS at a scratch copy of settings.json; that is how the mutation +# runs that prove these cases can fail are driven (see NON-VACUITY at the foot of this file). +# Both default to the real files, so a plain invocation checks the real hook and real wiring. + +set -uo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +hook="${GUARD_MAIN_CHECKOUT_HOOK:-$here/guard-main-checkout.sh}" +settings="${GUARD_MAIN_CHECKOUT_SETTINGS:-$here/../settings.json}" +pass=0 +fail=0 +skip=0 + +command -v jq >/dev/null 2>&1 || { echo "selftest needs jq to build payloads" >&2; exit 1; } +command -v git >/dev/null 2>&1 || { echo "selftest needs git to build the fixture" >&2; exit 1; } +[ -x "$hook" ] || { echo "hook not executable: $hook" >&2; exit 1; } + +# --- fixture --------------------------------------------------------------------------- +# MAIN a shared PRIMARY checkout WT a linked worktree of MAIN +# SIB a SECOND primary checkout SIBWT a linked worktree of SIB +# PLAIN a directory inside no repo at all ODD a PRIMARY checkout whose own path +# carries a literal `worktrees` segment +tmp="$(mktemp -d)" +nojq="" +trap 'rm -rf "$tmp" ${nojq:+"$nojq"}' EXIT INT TERM +MAIN="$tmp/mainrepo" +WT="$tmp/mainrepo-task" +SIB="$tmp/siblingrepo" +SIBWT="$tmp/siblingrepo-task" +PLAIN="$tmp/plain" +ODD="$tmp/worktrees/oddrepo" +mkdir -p "$MAIN/pkg/deep" "$SIB/pkg" "$PLAIN" "$ODD/pkg" +( + for r in "$MAIN" "$SIB" "$ODD"; do + cd "$r" || exit 1 + git init -q . + git config user.email selftest@example.com + git config user.name selftest + : > README.md + mkdir -p pkg + : > pkg/x.ts + : > pkg/x.ipynb + git add -A + git commit -qm init + done + cd "$MAIN" && git worktree add -q "$WT" -b selftest-wt + cd "$SIB" && git worktree add -q "$SIBWT" -b selftest-sib-wt +) >/dev/null 2>&1 || { echo "could not build the git fixture" >&2; exit 1; } + +CWD="$PLAIN" # the hook's PROCESS cwd for the cases that follow; reassigned per section +PROJ="$PLAIN" # CLAUDE_PROJECT_DIR for the cases that follow; reassigned per section + +short() { # short -> fixture paths rendered as their variable names + local s="$1" + # longest paths first: $WT and $SIBWT have $MAIN and $SIB as prefixes + s="${s//$SIBWT/\$SIBWT}"; s="${s//$WT/\$WT}"; s="${s//$SIB/\$SIB}" + s="${s//$MAIN/\$MAIN}"; s="${s//$PLAIN/\$PLAIN}"; s="${s//$ODD/\$ODD}" + s="${s//$tmp/\$tmp}" + printf '%s' "$s" +} + +verdict() { # verdict [env…] -> block | allow | exitN + local payload="$1"; shift + local rc + ( cd "$CWD" && printf '%s' "$payload" | env CLAUDE_PROJECT_DIR="$PROJ" "$@" "$hook" >/dev/null 2>&1 ) + rc=$? + case "$rc" in + 0) printf 'allow' ;; + 2) printf 'block' ;; + *) printf 'exit%s' "$rc" ;; + esac +} + +check() { # check