Skip to main content
One canonical home per artifact. No per-repo duplication of hook definitions or shared lint configs.
Across the workspace, dozens of repos had their own .pre-commit-config.yaml with the same baseline hooks rewritten everywhere, plus shared lint configs (.markdownlint, .tflint.hcl, .ansible-lint, .yamllint) copy-pasted across multiple repos. The shared pre-commit architecture eliminates that duplication so a single source of truth drives every consumer.

Canonical homes

Six profiles

Pick one per consumer repo. base, nix, and markdown are deliberately identical modules; the name signals intent to the reader.

Consumer pattern — Nix path

Scaffold this layout into a new repo via:

Consumer pattern — non-Nix path

Why one canonical home per artifact

  • Single update propagates everywhere. nix flake update in the consumer (or in nix-devenv for cross-org propagation) pulls every hook to a new pinned version. No per-repo rev: bumps. (Non-Nix consumers instead get Renovate rev: bumps, scheduled and auto-merged by trust tier — see Dependency automation.)
  • Drift dies. Before consolidation the inventory found pre-commit-terraform pinned at six different revs across the workspace and markdownlint-cli at four. Canonical pinning eliminates that.
  • New repo onboarding is one line. nix flake init -t github:dryvist/nix-devenv#with-hooks plus picking a profile, no copy-paste of 30 lines of YAML.

What stays per-repo

The architecture explicitly does NOT centralise everything. Hooks with low coverage in the inventory stay opt-in per repo because they have high false-positive rates or dominate the hook cycle:
  • checkov (terraform security) — appears in 3 inventory repos; opt-in via pre-commit.settings.hooks.checkov.enable = true;
  • bandit (Python security) — appears in 1 repo
  • detect-secrets — appears in 1 repo
  • AWS / GCP / Azure tflint plugins — repo-targeting; canonical tflint.hcl enables only the core terraform plugin

Rules

  • Don’t add hook definitions to a consumer-repo .pre-commit-config.yaml. If the canonical profile doesn’t cover something, add it to nix-devenv’s base profile or the matching language profile, then pull it through everywhere on the next nix flake update.
  • Don’t duplicate shared lint config files (.markdownlint, .tflint.hcl, .ansible-lint, .yamllint). Pull them via the Nix path (fetch-shared-configs) or copy at scaffold from dryvist/.github (non-Nix path).
  • A clean migration PR adds flake.nix + flake.lock, modifies .envrc, deletes .pre-commit-config.yaml, and deletes the duplicated lint config files that the canonical now covers.
For AI agents, these decisions are codified in the on-demand pre-commit-architecture skill (claude-code-plugins, git-workflows plugin), which loads when an agent edits pre-commit config or scaffolds hooks.

Known limitations

  • cachix/git-hooks.nix’s built-in tflint wrapper drops args beyond $1, so the terraform profile’s --config <sharedConfigs.tflint> plumbing doesn’t reach tflint. Consumers either keep a synced copy of the canonical .tflint.hcl in the repo (tflint’s local-config discovery finds it) or override tflint.args to lib.mkForce [ ].
  • terraform-validate hooks need network access to tofu init external modules. nix flake check runs hooks in a sandboxed environment without network. Repos with external module references override terraform-validate.enable = lib.mkForce false for the flake-check path and run tofu init -backend=false plus tofu validate in CI.
  • gitleaks is not in cachix/git-hooks.nix’s built-in hook set yet. Consumers wire it as a custom hook locally; a follow-up adds it to the base profile.

References