Git hooks

Git fires scripts at deterministic points (pre-commit, commit-msg, pre-push, post-merge, …) and a blocked exit code vetoes the action. That’s the whole mechanism — everything else is how teams get the same hooks onto every clone and keep them fast. The core problem: .git/hooks/ is not versioned and not distributed — fresh clones start hookless. Every hook manager is an answer to “how do hooks travel with the repo?” plus “how do they stay fast enough that nobody types --no-verify?”.

The distribution mechanisms

  • Bootstrap script / prepare entrypoint (npm convention) — a package-manager lifecycle step clones/symlinks hooks on install. Simple, language-bound (husky’s route).
  • Single-binary config in repo root — .pre-commit-config.yaml / lefthook.yml / hk config; a binary installed once reads it and installs itself into the hooks path. Language-agnostic, the modern default.
  • core.hooksPath — point git at a versioned directory in the repo (.githooks/ + one-line setup). Zero dependencies, transparent shell scripts; the KISS option. Underrated for simple needs.
  • Git ≥2.54 native config hooks ([hook "x"] in gitconfig, 2026-04) — personal/global hooks without any manager; but .git/config isn’t versioned, so it doesn’t solve team distribution.

The managers (candidates, none chosen yet)

  • prek (OSS) — Rust drop-in pre-commit replacement: same .pre-commit-config.yaml, same hook registry, parallel execution, millisecond startup, built-in in-place fixes. The bet: keep pre-commit’s ecosystem, delete its Python cost.
  • pre-commit (Yelp) — the incumbent. Largest hook ecosystem, per-hook isolated venvs, autoupdate. Cost: Python required, slow first run, per-hook-repo supply-chain surface (moving tags — --freeze mitigates).
  • Husky — the JS standard: hooks as plain shell scripts in .husky/, wired by npm prepare. No parallelism or staged-file filtering without adding lint-staged.
  • Lefthook — Go single binary, lefthook.yml, native parallel execution, {staged_files} glob filtering, local override files. The polyglot performance option.
  • hk (OSS) — the jdx (mise author) entry, Rust: linters/formatters run in parallel under read/write file locks (independent tools concurrent, same-file tools take turns), and the same steps run in git hooks, from the terminal, and in CI — one config, three contexts, killing the “hooks pass locally, CI checks something else” drift.

Measured reality (third-party benchmarks, 500+ files): lefthook warm <1s; husky+lint-staged 2-3s; pre-commit 4-6s (venv overhead). prek claims parity with pre-commit at lefthook-class speed; hk’s lock-based parallelism is the newest datapoint.

The open head-to-head: prek vs hk — prek bets on pre-commit config/ecosystem reuse, hk on hooks=CLI=CI unification. Decision pending hands-on.

Which hook, when (working position)

jyn.dev — pre-commit hooks are fundamentally broken (capture: clippings/jyn-dev-pre-commit-hooks-are-fundamentally-broken.md) argues pre-commit hooks are the wrong default: they gate your commit on your machine for things CI will check anyway, the slowness trains --no-verify, and hooks that mutate files mid-commit race with the user. Working position adopted here (KRZ): run hooks at pre-push, with one exception — credential scanning at pre-commit, because a secret that lands in a commit is in history forever; style/lint can wait for push, leaked tokens cannot. That also matches the “hooks may fix, one writer per pass” rule below.

Video companions (transcripts captured): “Git Will Finally Make Sense After This” (LearnThatStack — commits are snapshots, the DAG, refs as pointers: sources/Git Will Finally Make Sense After This.md) and Jessica Kerr’s “Git Happens” (JDD — vocabulary-first git model, why the man pages defeat newcomers: sources/Jessica Kerr Git Happens.md). “You’ve Been Using Git Wrong” (Tom Delalande) covers where CI hooks should run in the Git-vs-GitHub paradigm conflict (sources/You've Been Using Git Wrong.md).

Design rules (from the garden’s own hook + Cilium’s CI doctrine)

  • Hook what the project actually cares about (the garden’s frontmatter check validates YAML contracts), not generic hygiene theater.
  • A hook may fix (insert keys, bump updated:) on staged files — but one writer per pass; two auto-fixers racing corrupts files.
  • Parallelism needs file locks or staged-file partitioning — otherwise linters and formatters fight over the same files (hk’s whole thesis).
  • Supply-chain: every manager adds an install-time execution surface; pin hook repos to immutable SHAs (--freeze), respect the cooldown logic from the Golden repo CI section.
  • CI parity is a feature: if the hook config and the CI lint job are different artifacts, they will drift — hk’s three-context unification (or just running the same script both places) is the fix.
  • The --no-verify escape hatch is a pressure gauge: frequent bypasses mean hooks are too slow or too preachy — fix the hook, don’t blame the user.

Related: Golden repo (the rules this note feeds) · prek (OSS) · hk (OSS) · Diff tools (what a hook might run) · Agent tests (hooks as the first automated gate in the testing ladder) · Git recovery · Git commit messages · Git housekeeping · Git worktrees