Skip to content

Commit-time gates (pre-commit)

Every drift gate runs at some point in the lifecycle of a change: at commit, at PR/CI, at compile, or at runtime. Earlier is cheaper — a gate that fails on git commit costs seconds; the same failure caught in production costs an incident. The pre-commit subsystem is the earliest tier: hooks that run on every commit, before code ever reaches CI.

This page is the canonical reference for that subsystem. It is the commit-time slice of the broader gate wall described in the glossary.

A gate’s value is inseparable from when it fires.

TierFires onFeedback latencyExample gates
Commit-timegit commit (pre-commit hooks)seconds, localsecret scan, buf lint, migration discipline
CI / PR-timeevery pushed PRminutes, remotecheck-vocab, check-mocks, check-sqlc, the full lint umbrella
Compile-timethe buildinside the buildvar _ ServiceHandler = (*Service)(nil), exhaustive matchState
Runtimea request, a bootproductionDB CHECK constraints, config Validate() at startup

A gate placed at commit-time gives the tightest loop a human ever gets: the mistake is refused before it is even recorded. The same rule, left to runtime, is an outage.

Seven hooks run on every commit (.pre-commit-config.yaml). They are deliberately the checks that are cheap to run locally and expensive to discover late.

HookGatesWhy it runs at commit-time
scan-secretsgitleaks + trufflehog — no secret reaches a commitA leaked secret is unrecoverable once pushed; the only safe place to catch it is before the commit object exists.
strip-ai-coauthorsremoves AI co-author trailers from messagesA message-shape rule with no reason to wait for CI.
buf-lintthe proto contract stays well-formedThe contract is the BE↔FE seam; a malformed proto should never enter history.
migration-disciplinemigration naming + append-onlyAn edited applied migration is a data-integrity hazard; refuse it at the source.
squawk-migrationsmigration backwards-compatibilityA backwards-incompatible migration is caught before it can be reviewed as if it were safe.
check-connect-errorsconnect.NewError only inside errorkit/Keeps the typed-error boundary intact — one place translates application failures into wire failures.
check-sql-scopeowner-scoped tables require a user_id filterA missing tenant filter is a security bug; the cheapest place to refuse it is before commit.

The hooks are wired by the standard dev bootstrap and managed with make:

Terminal window
make hooks-install # wire the git hooks (bootstraps pre-commit into a project-local .venv)
make hooks-check # run the hooks against the working tree without committing
make hooks-uninstall # remove the hooks

pre-commit runs in an isolated project-local .venv — it never touches a global Python. buf and squawk come with the pnpm install; gitleaks/trufflehog run from Docker, and the hook degrades gracefully (skips with a warning) when Docker is unavailable.

Commit-time and CI-time are not redundant; they are the same discipline at two latencies. Some gates appear at both tiers on purpose: buf lint, check-connect-errors, and the migration checks run as pre-commit hooks and in CI, so a contributor who bypasses local hooks (git commit --no-verify) is still caught at the PR. The commit-time copy exists for the feedback loop; the CI copy exists for the guarantee.

The full gate wall — across commit, CI, compile, and runtime — is collected in Are We Drifting? Part 14.