Pinning and autoupdating pre-commit hook versions Jump to heading
Every entry in .pre-commit-config.yaml points at a Git repository and a rev. That revision decides exactly which code runs on every developer’s machine at every commit — it is a dependency, and an unusually privileged one, since it executes with the developer’s permissions inside their working copy. Teams often pin it loosely or never update it: a tag that could be moved, a branch name that changes daily, or a version from three years ago that formats code differently from the CI image. This page sets up pins that cannot move under you, an update routine that keeps them current, and a review step that treats hook updates with the seriousness they deserve, within the pre-commit framework for polyglot repos.
When to use this approach Jump to heading
- Your repository uses the pre-commit framework with hooks from external repositories.
- Some
revvalues are branch names, short tags or very old versions. - Formatting churn appears because developers or CI run different hook versions.
- You already pin other dependencies by hash, as in pinning GitHub Actions to a commit SHA, and want the same discipline here.
Step 1 — Audit what each hook is pinned to Jump to heading
List each repository and its rev, and classify it: a full commit hash is immutable; a version tag is conventional but movable; a branch name changes constantly.
python3 - <<'EOF'
import re, yaml
cfg = yaml.safe_load(open(".pre-commit-config.yaml"))
for r in cfg["repos"]:
rev = r.get("rev", "")
kind = "local/meta" if r["repo"] in ("local", "meta") else \
"hash" if re.fullmatch(r"[0-9a-f]{40}", rev) else \
"tag" if re.match(r"v?\d", rev) else "BRANCH?"
print(f"{kind:10} {rev:42} {r['repo']}")
EOF Step 2 — Pin to commit hashes with the version as a comment Jump to heading
pre-commit autoupdate --freeze resolves each tag to its commit and writes the hash, leaving the tag as a comment so humans can still read it.
pre-commit autoupdate --freeze
git diff .pre-commit-config.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-yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: 8b5112a3b2ad121439a2092f8ff548c0d80f2514 # frozen: v0.6.9
hooks:
- id: ruff
args: [--fix]
- id: ruff-format The hashes shown here are illustrative; autoupdate --freeze writes the real commit for each tag.
# Verification: every external repo now has a 40-character rev
grep -E '^\s+rev:' .pre-commit-config.yaml | grep -vE '[0-9a-f]{40}' || echo "all frozen" Step 3 — Update on a schedule, not by accident Jump to heading
Pinned hooks never update unless someone updates them. A scheduled job runs autoupdate --freeze weekly and opens a pull request when anything changed.
# .github/workflows/pre-commit-autoupdate.yml
on:
schedule: [{ cron: "37 5 * * 1" }]
workflow_dispatch:
permissions: { contents: write, pull-requests: write }
jobs:
autoupdate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pipx install pre-commit && pre-commit autoupdate --freeze
- run: pre-commit run --all-files || true # apply any new formatting
- run: |
git diff --quiet && exit 0
git switch -c chore/pre-commit-autoupdate-$(date +%F)
git commit -am "chore: update pre-commit hooks"
git push -u origin HEAD
gh pr create --fill --label dependencies
env: { GH_TOKEN: "${{ github.token }}" } Running all hooks after updating matters: a new formatter version may reformat files, and those changes belong in the same pull request as the version bump. Otherwise the next unrelated commit picks them up and its diff fills with noise.
Step 4 — Review hook updates as code changes Jump to heading
A hook update changes code that runs on every developer’s machine. Review it as you would a dependency with install scripts: read the upstream changelog, look at the diff between the old and new commit for anything surprising, and check the hook’s repository is still the one you meant.
# What changed upstream between the old and new pins?
old=2c9f8759...; new=0ad5f61d...
tmp=$(mktemp -d); git -C "$tmp" init -q
git -C "$tmp" fetch -q https://github.com/pre-commit/pre-commit-hooks "$old" "$new"
git -C "$tmp" log --oneline "$old..$new"
git -C "$tmp" diff --stat "$old" "$new" Treat a hook that suddenly adds network calls, install scripts or new dependencies with suspicion, just as you would a package dependency.
Step 5 — Keep CI on the same versions Jump to heading
CI must run the same frozen versions developers do, or the two disagree about formatting. Running pre-commit run in CI reads the same config, so this is automatic — as long as CI does not override hook versions or use a separately installed copy of the tools.
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pipx install pre-commit && pre-commit run --show-diff-on-failure --color=always --all-files Caching the hook environments keeps this fast; see caching pre-commit environments in CI.
Step 6 — Pin the framework itself Jump to heading
The pre-commit tool has its own version, and old versions may not understand newer config features. Set a minimum in the config and pin the version installed in CI.
# .pre-commit-config.yaml
minimum_pre_commit_version: "3.7.0" pipx install "pre-commit==3.8.0" Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does freezing make updates harder? Jump to heading
No. pre-commit autoupdate --freeze reads the tags upstream, picks the newest, and writes its hash, so updating frozen pins is the same command as updating tags.
What about the local repo type? Jump to heading
Local hooks run code from your own repository and have no rev, so there is nothing to pin. Their tools may still need pinned versions through the language’s own dependency files.
Can Renovate or Dependabot update pre-commit hooks? Jump to heading
Renovate supports pre-commit configuration natively. Either a bot or the scheduled job above works; what matters is that updates arrive as reviewed pull requests rather than on someone’s laptop.
Related Jump to heading
- The pre-commit Framework for Polyglot Repos — the parent topic.
- Sharing One Hook Config Across Repositories — keeping pins consistent across many repos.
- Grouping Dependency Updates to Reduce PR Noise — the same review discipline for other dependencies.
- Pinning Git Dependencies to Commit SHAs — the supply-chain reasoning behind hash pins.