#!/usr/bin/env bash # Claude Code PreToolUse(Bash) hook: refuse `git push` until the local CI gates pass. # # Reads the hook payload on stdin. Non-push commands exit immediately, so the # overhead on ordinary Bash calls is one short script invocation. # # On failure it emits a PreToolUse deny decision, which blocks the push and hands # the reason back to the model so it can fix the problem instead of retrying. set -uo pipefail PAYLOAD="$(cat)" CMD="$(printf '%s' "$PAYLOAD" | jq -r '.tool_input.command // ""' 2>/dev/null)" # Only gate actual pushes (covers `git push`, `git -C push`, flags, etc.). # Deliberately ignores --dry-run. if ! printf '%s' "$CMD" | grep -Eq '(^|[;&|]|\s)git(\s+-[^;&|]*)?\s+push(\s|$)'; then exit 0 fi if printf '%s' "$CMD" | grep -q -- '--dry-run'; then exit 0 fi REPO="/Users/rteslow/mikrotik-manager" [[ -x "$REPO/scripts/ci-preflight.sh" ]] || exit 0 # nothing to enforce if OUTPUT="$("$REPO/scripts/ci-preflight.sh" 2>&1)"; then exit 0 fi DETAIL="$(tail -60 /tmp/ci-preflight-failures 2>/dev/null)" python3 - "$OUTPUT" "$DETAIL" <<'PY' import json, sys summary, detail = sys.argv[1], sys.argv[2] reason = ( "Push blocked: the local CI gates failed, so this would fail on GitHub too.\n\n" f"{summary.strip()}\n\n{detail.strip()}\n\n" "Fix these, re-run scripts/ci-preflight.sh until it passes, then push." ) print(json.dumps({ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": reason, } })) PY exit 0