Sharing one hook config across repositories Jump to heading
An organisation with forty repositories usually has forty slightly different .pre-commit-config.yaml files. One pins Ruff from last year, another forgot the secret scanner, a third has a custom hook copied from a fourth with a bug that was fixed there months ago. The pre-commit framework has no “inherit from” mechanism, so consistency has to be built. Two pieces do most of the work: a central repository that publishes your organisation’s own hooks, and a baseline configuration synchronised into every repository by automation, with a clearly marked place for each repository’s additions. This page sets both up, within the pre-commit framework for polyglot repos.
When to use this approach Jump to heading
- Many repositories use the pre-commit framework and their configurations have drifted.
- Your organisation has custom hooks — policy checks, naming rules — copied between repositories.
- A security requirement, such as secret scanning on every commit, must apply everywhere.
- Hook versions are pinned per repository, as in pinning and autoupdating pre-commit hook versions, and you want those pins coordinated.
Step 1 — Publish organisation hooks from one repository Jump to heading
Move custom hooks into a dedicated repository with a .pre-commit-hooks.yaml manifest. Every repository then references it like any third-party hook source, and fixes reach everyone through normal version updates.
# acme/pre-commit-hooks — .pre-commit-hooks.yaml
- id: forbid-prod-urls
name: forbid production URLs in code
entry: scripts/forbid-prod-urls.sh
language: script
types: [text]
- id: check-service-manifest
name: validate service.yaml
entry: check-service-manifest
language: python
files: ^service\.yaml$ # Tag releases so consumers can pin and autoupdate
git tag -a v1.4.0 -m "forbid-prod-urls: ignore test fixtures" && git push origin v1.4.0 Step 2 — Define a baseline configuration Jump to heading
Write the hooks every repository must run — whitespace and YAML checks, secret scanning, your organisation hooks — as a baseline file in a central repository. Keep it minimal: anything language-specific belongs in the repositories that need it.
# acme/pre-commit-baseline — baseline.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0
hooks: [{ id: trailing-whitespace }, { id: end-of-file-fixer }, { id: check-merge-conflict }]
- repo: https://github.com/gitleaks/gitleaks
rev: 77c3c6a34b2577d71083442326c60b8fd58926ec # frozen: v8.18.4
hooks: [{ id: gitleaks }]
- repo: https://github.com/acme/pre-commit-hooks
rev: 5d1e0c7a9f3b2e4d6c8a0b1f3e5d7c9a1b3e5f70 # frozen: v1.4.0
hooks: [{ id: forbid-prod-urls }] The pinned hashes are placeholders for illustration; generate real ones with pre-commit autoupdate --freeze in the baseline repository.
Step 3 — Merge baseline and local additions with a sync job Jump to heading
Each repository keeps its own additions in a separate file. A sync job combines the baseline with the local file into the real .pre-commit-config.yaml, which is generated and marked as such.
# sync_precommit.py — baseline + .pre-commit-local.yaml -> .pre-commit-config.yaml
import yaml, urllib.request
base = yaml.safe_load(urllib.request.urlopen(
"https://raw.githubusercontent.com/acme/pre-commit-baseline/main/baseline.yaml"))
try:
local = yaml.safe_load(open(".pre-commit-local.yaml")) or {}
except FileNotFoundError:
local = {}
cfg = {"minimum_pre_commit_version": "3.7.0",
"repos": base["repos"] + local.get("repos", [])}
with open(".pre-commit-config.yaml", "w") as f:
f.write("# GENERATED from acme/pre-commit-baseline + .pre-commit-local.yaml — do not edit\n")
yaml.safe_dump(cfg, f, sort_keys=False) # Verification: regenerating produces no diff
python3 sync_precommit.py && git diff --exit-code .pre-commit-config.yaml Step 4 — Roll baseline changes out with pull requests Jump to heading
When the baseline changes, a job in the baseline repository runs the sync in every consumer and opens a pull request in each. Repository owners review and merge, so a baseline change never lands unreviewed.
# From the baseline repository's CI, for each consumer repository
for repo in $(cat consumers.txt); do
gh repo clone "$repo" "/tmp/$repo" -- -q --depth 1
( cd "/tmp/$repo" && python3 ../sync_precommit.py &&
git diff --quiet || {
git switch -c chore/pre-commit-baseline-$(date +%F)
git commit -am "chore: sync pre-commit baseline"
git push -u origin HEAD && gh pr create --fill --label tooling
} )
done Bulk operations across many repositories are covered further in bulk updating repository settings with the API.
Step 5 — Fail CI on drift Jump to heading
A generated file can still be edited by hand. CI regenerates it and fails if the committed version differs, which also catches a repository that removed a baseline hook.
- name: pre-commit config is in sync with baseline
run: python3 sync_precommit.py && git diff --exit-code .pre-commit-config.yaml Step 6 — Measure coverage across the organisation Jump to heading
Track which repositories have adopted the baseline and which are behind. A short report from the forge’s API is enough.
for repo in $(gh repo list acme --limit 500 --json nameWithOwner --jq '.[].nameWithOwner'); do
head=$(gh api "repos/$repo/contents/.pre-commit-config.yaml" --jq .content 2>/dev/null | base64 -d 2>/dev/null | head -1)
case "$head" in "# GENERATED"*) echo "synced $repo" ;; "") echo "none $repo" ;; *) echo "manual $repo" ;; esac
done | sort | uniq -c -w 9 Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Can a repository opt out of one baseline hook? Jump to heading
Allow it explicitly: support an exclude_baseline: [hook-id] list in the local file, applied by the sync script and visible in review. An explicit, reviewed exception is far better than a silent deletion.
Why not use a single shared config via a URL? Jump to heading
pre-commit reads only the local file and does not support remote includes. Generating the file keeps the framework unchanged and works offline once synced.
How do we handle repositories in different languages? Jump to heading
The baseline holds only language-neutral hooks. Language-specific hooks go in local files, or in optional baseline fragments — baseline-python.yaml, baseline-go.yaml — that a repository opts into by name.
Related Jump to heading
- The pre-commit Framework for Polyglot Repos — the parent topic.
- Writing a Custom Local pre-commit Hook — hooks worth promoting to the central repository.
- Shipping a Team gitconfig with includeIf — the same baseline-plus-local pattern for Git configuration.
- Managing Repository Permissions as Code — another organisation-wide reconcile loop.