Skip to content

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.yaml format, 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 the ENC[AES256_GCM,...] ciphertext blocks on every edit.
  • clusters/local/flux-system/gotk-components.yaml — auto-generated by flux bootstrap, never hand-edited; formatting it produces a large diff with no value.

commitlint Conventional Commits Format

All commit messages must follow the pattern:

type(scope): short description

optional body

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 log and 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 install to be set up

Negative

  • prek run on 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 add again before the commit lands (standard fixer-hook behaviour)
  • Developers must run prek install after 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