ADR-013: prek for Pre-commit Hooks¶
Status¶
Accepted
Date¶
2026-03-21
Context¶
The repository had no automated guardrails on commits. Several issues had already occurred or been identified as risks:
- Plaintext credentials were committed to Git on multiple occasions (
identity/keycloak.yaml,observability/grafana-oidc-secret.yaml), requiring a destructive history rewrite to remove them. - YAML files had inconsistent formatting (trailing whitespace, missing newlines, inconsistent indentation).
- Markdown docs accumulated style violations (missing code fence languages, bare URLs, inconsistent list numbering).
- Commit messages were free-form with no enforced convention, despite the project having consistently used Conventional Commits from the start.
A pre-commit framework prevents these classes of problem from entering the repository rather than discovering them after the fact.
Decision¶
Use prek — a Rust-based reimplementation of the pre-commit framework — as the hook runner.
prek was chosen over the original Python pre-commit tool because:
- Automatic toolchain management: prek installs Go, Node, Python, and Rust environments itself. No system-level installations are required beyond prek itself.
- Performance: Rust-based runner is significantly faster than the Python-based original.
- Full compatibility: Uses the identical
.pre-commit-config.yamlformat, so all existing hook repositories work without modification.
Hooks Configured¶
Pre-commit stage — runs on every git commit:
| Hook | Source | Purpose |
|---|---|---|
trailing-whitespace |
builtin | Remove trailing whitespace |
end-of-file-fixer |
builtin | Ensure newline at end of file |
check-yaml --allow-multiple-documents |
builtin | Validate YAML syntax (multi-doc Kubernetes manifests supported) |
check-json |
builtin | Validate JSON syntax |
check-merge-conflict |
builtin | Block accidental merge markers |
check-added-large-files (500 KB) |
builtin | Block large binary files |
detect-private-key |
builtin | Block raw private key material |
gitleaks |
gitleaks/gitleaks | Scan for secrets, credentials, API keys |
yamlfmt |
google/yamlfmt | Auto-format YAML (acts as fixer) |
markdownlint-cli2 |
DavidAnson/markdownlint-cli2 | Lint Markdown / ADR docs |
Commit-msg stage — runs after the commit message is written:
| Hook | Source | Purpose |
|---|---|---|
commitlint |
alessandrojcm/commitlint-pre-commit-hook | Enforce Conventional Commits format |
Supporting Config Files¶
| File | Purpose |
|---|---|
.pre-commit-config.yaml |
Hook definitions and versions |
.yamlfmt |
yamlfmt: 2-space indent, LF endings, excludes SOPS and auto-generated files |
.commitlintrc.yaml |
commitlint: @commitlint/config-conventional, 120-char subject limit |
.markdownlint.yaml |
markdownlint: ADR-friendly rule set (MD022/031/032/046/060 relaxed) |
SOPS and Auto-generated Files Excluded from yamlfmt¶
yamlfmt is excluded from two file categories to avoid noise:
*.sops.yaml— reformatting SOPS-encrypted files does not break them (YAML is still valid after formatting), but produces large diffs against theENC[AES256_GCM,...]ciphertext blocks on every edit.clusters/local/flux-system/gotk-components.yaml— auto-generated byflux bootstrap, never hand-edited; formatting it produces a large diff with no value.
commitlint Conventional Commits Format¶
All commit messages must follow the pattern:
Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, security.
Scope is optional. Subject line limit is 120 characters (relaxed from the default 72 to accommodate descriptive GitOps commit messages with file paths and ticket references).
Alternatives Considered¶
| Option | Reason Not Chosen |
|---|---|
Original pre-commit (Python) |
Requires Python on the system; prek manages all runtimes itself and is faster |
| Husky (Node.js) | Requires Node on the system; designed for Node projects; no multi-language toolchain management |
| GitHub Actions only (CI-side checks) | Provides no local feedback loop; developers discover failures only after pushing |
| lefthook | Good tool, but less ecosystem compatibility than the pre-commit hook format |
Consequences¶
Positive¶
- Credentials and private keys are blocked before they reach git history
- YAML is auto-formatted on every commit — no manual formatting required
- Markdown docs stay lint-clean as they are written
- Conventional commit format is enforced — consistent
git logand changelog generation - All tooling is managed by prek — no system-level Node, Go, or Python required
- Hooks are defined in the repo — any contributor clones and runs
prek installto be set up
Negative¶
prek runon a clean checkout downloads and builds tool environments on first run (yamlfmt requires Go build ~30 s, commitlint requires Node install ~10 s); subsequent runs use the cache- yamlfmt may reformat files on commit, requiring
git addagain before the commit lands (standard fixer-hook behaviour) - Developers must run
prek installafter cloning; the hooks are not automatically active
Setup Instructions¶
# Install prek
brew install prek
# Wire hooks into the local git repo (run once after cloning)
prek install # pre-commit stage
prek install --hook-type commit-msg # commit-msg stage for commitlint
# Run all hooks manually against all files
prek run --all-files
# Run a specific hook
prek run yamlfmt
prek run gitleaks
See also: .pre-commit-config.yaml