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
| Artifact | Canonical home | How consumers pull it |
|---|---|---|
| Hook definitions (Nix path) | dryvist/nix-devenv lib/pre-commit-hooks.nix + flake-modules/profiles/<name>.nix | imports = [ inputs.nix-devenv.flakeModules.<profile> ] in the consumer’s flake.nix |
| Hook definitions (non-Nix path) | dryvist/.github precommit/templates/<profile>.yaml | Copy at scaffold; Renovate keeps rev: pins fresh |
.markdownlint-cli2.yaml | dryvist/.github at the root | Same root location pre-dates the precommit layer; kept for the existing markdownlint workflow’s benefit |
.tflint.hcl / .ansible-lint / .yamllint.yml | dryvist/.github precommit/configs/ | Nix path: nix-devenv.lib.fetch-shared-configs materialises nix-store paths; non-Nix: copy at scaffold |
zizmor.yml workflow-security policy | dryvist/.github at the root | Passed as --config by the base profile’s zizmor hook |
Six profiles
Pick one per consumer repo.| Profile | Adds on top of base hygiene | Matches |
|---|---|---|
base | Generic file hygiene (whitespace, EOL, YAML/JSON/TOML, large files, private keys, merge conflicts), markdownlint-cli2, zizmor, treefmt | Most repos |
nix | Alias for base — deadnix and statix already in base file-glob to .nix automatically | nix-* repos |
markdown | Alias for base — markdownlint-cli2 already in base file-globs to .md automatically | Markdown-heavy repos |
terraform | terraform-format, terraform-validate, tflint | terraform-*, tofu-* |
ansible | ansible-lint, yamllint | ansible-* |
python | ruff, ruff-format, mypy | Python repos |
base, nix, and markdown are deliberately identical modules; the name signals intent to the reader.
Consumer pattern — Nix path
Consumer pattern — non-Nix path
Why one canonical home per artifact
- Single update propagates everywhere.
nix flake updatein the consumer (or in nix-devenv for cross-org propagation) pulls every hook to a new pinned version. No per-reporev:bumps. - Drift dies. Before consolidation the inventory found
pre-commit-terraformpinned at six different revs across the workspace andmarkdownlint-cliat four. Canonical pinning eliminates that. - New repo onboarding is one line.
nix flake init -t github:dryvist/nix-devenv#with-hooksplus 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 viapre-commit.settings.hooks.checkov.enable = true;bandit(Python security) — appears in 1 repodetect-secrets— appears in 1 repo- AWS / GCP / Azure
tflintplugins — repo-targeting; canonicaltflint.hclenables 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 tonix-devenv’s base profile or the matching language profile, then pull it through everywhere on the nextnix 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 fromdryvist/.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.
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-intflintwrapper drops args beyond$1, so theterraformprofile’s--config <sharedConfigs.tflint>plumbing doesn’t reach tflint. Consumers either keep a synced copy of the canonical.tflint.hclin the repo (tflint’s local-config discovery finds it) or overridetflint.argstolib.mkForce [ ].terraform-validatehooks need network access totofu initexternal modules.nix flake checkruns hooks in a sandboxed environment without network. Repos with external module references overrideterraform-validate.enable = lib.mkForce falsefor the flake-check path and rely on CI’s OIDC-authenticatedterragrunt validateto cover the check.gitleaksis not incachix/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
dryvist/nix-devenv— Nix-path canonicaldryvist/.githubprecommit/README.md— non-Nix-path canonical + architecture rationaledryvist/claude-code-pluginspre-commit-architectureskill — on-demand rule for AI agents- cachix/git-hooks.nix — upstream hook framework
- flake-parts — Nix flake composition framework