#!/usr/bin/env bash set -euo pipefail # Pre-commit hook to prevent committing restricted or sensitive data RESTRICTED_FILES="active_subs.json charges.json customers.json subscriptions.json" echo "Running sensitivity check..." for file in $RESTRICTED_FILES; do if git diff --cached --name-only | grep -q "^${file}$"; then echo "BLOCKED: restricted file pattern matched: ${file}" exit 1 fi done # Block sensitive file types from ever being committed SENSITIVE_EXTENSIONS="\.pem$|\.p12$|\.pfx$|\.key$|\.keystore$|\.jks$|\.enc$|id_rsa$|id_ed25519$|id_ecdsa$" if git diff --cached --name-only | grep -qE "$SENSITIVE_EXTENSIONS"; then echo "BLOCKED: sensitive file type staged for commit:" git diff --cached --name-only | grep -E "$SENSITIVE_EXTENSIONS" echo "If this is intentional (e.g. a template), use: git commit --no-verify" exit 1 fi # Gitleaks: comprehensive secret scanning (gracefully skips if not installed) if command -v gitleaks >/dev/null 2>&1; then echo "Running gitleaks secret scan..." if ! gitleaks protect --staged --config .gitleaks.toml --no-banner 2>/dev/null; then echo "BLOCKED: gitleaks detected secrets in staged changes." echo "Run 'gitleaks protect --staged --verbose' for details." exit 1 fi echo "Gitleaks scan passed." else # Fallback: broader pattern check when gitleaks is not installed # Covers Stripe, AWS, GCP, OpenAI, private keys, and generic high-entropy tokens STAGED_DIFF=$(git diff --cached | grep -E "^\+" | grep -v ".husky/pre-commit" | grep -v "_test\.go") if echo "$STAGED_DIFF" | grep -qE "(cus_|sub_|ch_|pi_|pm_|sk_live_|sk_test_|rk_live_|rk_test_|whsec_)"; then echo "WARNING: Potential Stripe identifiers found in staged changes." echo " Use 'git diff --cached' to review before proceeding." if [ -t 0 ]; then printf " Proceed anyway? (y/N): " read REPLY < /dev/tty echo if [ "$REPLY" != "y" ] && [ "$REPLY" != "Y" ]; then exit 1 fi else exit 1 fi fi if echo "$STAGED_DIFF" | grep -qE "(AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|sk-[a-zA-Z0-9]{20,}|AIza[0-9A-Za-z_-]{35})"; then echo "BLOCKED: Likely secret detected in staged changes (AWS key, private key, or API key)." echo " Use 'git diff --cached' to review." echo " Install gitleaks for more precise scanning: brew install gitleaks" exit 1 fi fi echo "Sensitivity check passed." STAGED_FILES="$(git diff --cached --name-only)" staged_files_match() { printf '%s\n' "$STAGED_FILES" | grep -qE "$1" } # Governance checks gating. # Run the full governance audit + Go test suite only when: # (a) the internal governance directory is present (skipped on fresh public-repo clones), AND # (b) the staged changes actually touch governance-relevant paths. # A pure frontend-only commit then skips the multi-minute Go test and Python # helper unit tests entirely. This matters when multiple agent harnesses # (Codex, Claude Code, Anti-Gravity) are committing in parallel against the # same tree; previously every commit serialized behind ~15-20 minutes of # audits regardless of what was actually staged. # # Governance-relevant paths: anything under docs/release-control/, the # release_control Python scripts, the repoctl Go package the audits live in, # any Go file (the Go test reads governance JSON via Go code), the module # manifests, the VERSION marker, and the husky hooks themselves (so changes # to this file still get validated). GOVERNANCE_PATHS='^docs/release-control/|^scripts/release_control/|^internal/repoctl/|\.go$|^go\.(mod|sum|work|work\.sum)$|^VERSION$|^\.husky/' if [ -f "docs/release-control/v6/internal/status.json" ] && staged_files_match "$GOVERNANCE_PATHS"; then echo "Running governance stage guard..." python3 scripts/release_control/governance_stage_guard.py echo "Running staged commit shape guard..." python3 scripts/release_control/staged_commit_shape_guard.py echo "Running control plane audit..." python3 scripts/release_control/control_plane_audit.py --check --staged echo "Running canonical completion guard..." python3 scripts/release_control/canonical_completion_guard.py echo "Running status audit..." python3 scripts/release_control/status_audit.py --check --staged echo "Running registry audit..." python3 scripts/release_control/registry_audit.py --check --staged echo "Running contract audit..." python3 scripts/release_control/contract_audit.py --check --staged echo "Running governance guardrail tests..." export PULSE_READ_STAGED_GOVERNANCE=1 go test ./internal/repoctl -count=1 echo "Running readiness assertion guard..." python3 scripts/release_control/readiness_assertion_guard.py --staged --active-target --proof-type automated echo "Running release-control helper unit tests..." python3 scripts/release_control/canonical_completion_guard_test.py python3 scripts/release_control/control_plane_audit_test.py python3 scripts/release_control/contract_audit_test.py python3 scripts/release_control/format_staged_go_test.py python3 scripts/release_control/governance_stage_guard_test.py python3 scripts/release_control/release_promotion_policy_support_test.py python3 scripts/release_control/registry_audit_test.py python3 scripts/release_control/readiness_assertion_guard_test.py (cd scripts/release_control && git -C ../.. show :scripts/release_control/release_promotion_policy_test.py | python3 -) python3 scripts/release_control/repo_file_io_test.py python3 scripts/release_control/staged_commit_shape_guard_test.py python3 scripts/release_control/status_audit_test.py python3 scripts/release_control/subsystem_contracts_test.py python3 scripts/release_control/subsystem_lookup_test.py unset PULSE_READ_STAGED_GOVERNANCE elif [ -f "docs/release-control/v6/internal/status.json" ]; then echo "Skipping governance checks (no staged governance, Go, repoctl, or hook changes)." else echo "Governance files not present, skipping governance checks." fi # Run Go formatting python3 scripts/release_control/format_staged_go.py # Run Go linting (if golangci-lint is available) if command -v golangci-lint >/dev/null 2>&1; then if staged_files_match '(^|/).*\.go$|(^|/)go\.(mod|sum|work|work\.sum)$|^\.golangci\.ya?ml$'; then echo "Running golangci-lint..." # Lint only packages containing staged Go files, not the entire repo STAGED_GO_PKGS=$(git diff --cached --name-only | grep '\.go$' | xargs -I{} dirname {} 2>/dev/null | sort -u | sed 's|^|./|' | tr '\n' ' ') if [ -z "$STAGED_GO_PKGS" ]; then STAGED_GO_PKGS="./..." fi # Report only issues introduced by the staged diff. Pre-existing # violations in touched packages do not block new commits, but any # new violation on a changed line still fails the commit. Override # with GOLANGCI_LINT_NEW_FROM_REV=HEAD~5 etc, or "" to lint everything. NEW_FROM_REV="${GOLANGCI_LINT_NEW_FROM_REV-HEAD}" NEW_FROM_FLAG="" if [ -n "$NEW_FROM_REV" ]; then NEW_FROM_FLAG="--new-from-rev=$NEW_FROM_REV" fi GOMAXPROCS="${GOMAXPROCS:-2}" golangci-lint run --concurrency "${GOLANGCI_LINT_CONCURRENCY:-2}" --timeout "${GOLANGCI_LINT_TIMEOUT:-5m}" $NEW_FROM_FLAG $STAGED_GO_PKGS else echo "Skipping golangci-lint (no staged Go/module/linter changes)." fi fi # Run frontend linting (if package.json has lint script) if [ -f frontend-modern/package.json ]; then if staged_files_match "^frontend-modern/"; then echo "Running frontend linter..." cd frontend-modern npm run lint cd .. else echo "Skipping frontend lint (no staged frontend changes)." fi fi echo "Pre-commit checks passed!"