Files
pulse/.husky/pre-commit
T
rcourtman 2e8bcad909 Gate heavy pre-commit governance checks on staged paths
The husky pre-commit hook unconditionally ran the full release-control
governance audit chain (`governance_stage_guard.py`, `control_plane_audit.py`,
`status_audit.py`, `registry_audit.py`, `contract_audit.py`, the
`go test ./internal/repoctl` package test, the readiness assertion guard,
and 15 Python helper unit tests) on every single commit, regardless of
what was actually staged. For a one-file frontend TS change, that meant
~15-20 minutes of audits the change couldn't possibly affect.

That cost is amplified by Pulse being driven by multiple agent harnesses
in parallel (Codex lanes, Claude Code interactive sessions, Anti-Gravity).
Each agent's commit serialized behind the same unrelated suite, even
when none of their staged files touched governance.

The hook already had `staged_files_match` and was using it to gate the
golangci-lint and `npm run lint` blocks. Apply the same pattern to the
governance block: run it only when the staged set actually touches
governance-relevant paths (docs/release-control/, scripts/release_control/,
internal/repoctl/, any .go file, go.{mod,sum,work,work.sum}, VERSION,
or the husky hooks themselves so hook changes still validate).

Frontend-only commits now finish in roughly the time the sensitivity
check, gitleaks scan, and `npm run lint` need (tens of seconds), rather
than tens of minutes. Governance-touching commits are unaffected; the
full audit chain still runs whenever any path in the gating regex is
staged.

Smoke-tested the regex against 13 representative paths (frontend TS,
README, internal docs JSON, release_control py, repoctl go, generic go,
go.mod/go.sum, VERSION, .husky/pre-commit, .husky/pre-push). All
classifications matched the intended skip/run behavior. Bash syntax of
the modified hook validated with `bash -n`.
2026-05-20 10:13:04 +01:00

180 lines
7.7 KiB
Bash
Executable File

#!/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!"