diff --git a/.claude/hooks/pre-commit-verification.sh b/.claude/hooks/pre-commit-verification.sh index c180acb..80b3425 100755 --- a/.claude/hooks/pre-commit-verification.sh +++ b/.claude/hooks/pre-commit-verification.sh @@ -1,12 +1,26 @@ #!/bin/bash # Hook: pre-commit-verification # Event: PreToolUse (Bash) -# Purpose: Ensure tests and linting pass before git commits +# Purpose: Run the project's detected quality gates before git commits and +# block the commit on failure. +# +# Enforcement rewrite (artifacts/plan_framework_hardening.md, unit U5b): this +# hook used to only ever print advisory text and trust a time-only stamp the +# AGENT was instructed to write by hand — self-attestation, not verification +# (see the plan's Design Principles). It now runs the gates itself. A stamp +# is trusted only when BOTH fresh (<=5 min) AND content-bound: its recorded +# tree-hash must equal the current `git write-tree` output, so a change +# staged seconds ago forces a re-run even if the last stamp is a minute old +# (review F8 — a time-only stamp rides post-edit changes). The stamp file is +# hook-authored only; nothing in this file's own output ever instructs the +# agent to write it. INPUT=$(cat) # jq is required to parse tool input; fail open if unavailable (hooks are -# guardrails, not a security boundary) +# guardrails, not a security boundary). This path is byte-for-byte the same +# as before this rewrite — no gates ever ran on the jq-absent path, and none +# do now either. command -v jq >/dev/null 2>&1 || exit 0 PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" @@ -18,48 +32,60 @@ if [ "$TOOL_NAME" != "Bash" ]; then exit 0 fi -# Check if this is a git commit command if ! echo "$COMMAND" | grep -qE '\bgit\s+commit\b'; then exit 0 fi -# Check for state file indicating verification already completed STATE_DIR="$PROJECT_DIR/.claude/hooks/.state" mkdir -p "$STATE_DIR" -VERIFICATION_FILE="$STATE_DIR/commit-verified" +STAMP_FILE="$STATE_DIR/commit-verified" -# If verification was completed recently (within last 5 minutes), allow commit -if [ -f "$VERIFICATION_FILE" ]; then - VERIFIED_TIME=$(cat "$VERIFICATION_FILE" 2>/dev/null || echo 0) - [[ "$VERIFIED_TIME" =~ ^[0-9]+$ ]] || VERIFIED_TIME=0 - CURRENT_TIME=$(date +%s) - TIME_DIFF=$((CURRENT_TIME - VERIFIED_TIME)) - - if [ "$TIME_DIFF" -lt 300 ]; then - # Verification is recent, allow commit - exit 0 - fi +# Escape hatch: unconditional, checked before the stamp or any gate ever +# runs, and always disclosed — never a silent skip. +if [ "${CLAUDE_SKIP_GATE_HOOK:-}" = "1" ]; then + cat << EOF +{ + "hookSpecificOutput": { + "additionalContext": "[GATE HOOK SKIPPED] CLAUDE_SKIP_GATE_HOOK=1 is set — quality gates were NOT run for this commit. Unset it to restore enforcement." + } +} +EOF + exit 0 fi # Detect project type and available gates — detection lives in gate-lib.sh -# (artifacts/plan_framework_hardening.md, unit U5a): one shared function -# emits invocable commands per gate; this hook only needs the human labels -# for its advisory text below, reconstructed here in the same order and -# format ("