Supply-chain audit: pinned vs. mutable references
The scenario
You now have two surfaces built across this track: the service graph from module 01 (a base image, potentially Dev Container Features) and the CI workflow from module 03, which references pre-commit hook repositories by their rev. Your security lead asks for a quick audit: which references, across these two surfaces, are actually pinned to an immutable point in time, and which can change content under the same name without anyone noticing?
Before you continue, one important scoping note: the only source that officially and precisely documents the immutability semantics you should expect here is pre-commit.com itself, regarding the rev field. For the other references (Docker image tags, GitHub Actions versions), the underlying principle is analogous but isn't guaranteed by that same documentation — treat that part as a reasoned extension of the same reasoning, not as a fact documented with the same authority.
Task: produce your audit before reading on
Open your files from modules 01 and 03 and, for every external reference they contain, note whether it points at something immutable or mutable. Do this yourself before reading the table below.
The excerpts below are a simplified example, not necessarily your files' exact content (your real .pre-commit-config.yaml may have different rev values or an extra hooks repo) — what matters is applying the same audit to your real file, not matching these exact values.
In your .pre-commit-config.yaml (module 03):
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
- repo: https://github.com/psf/black
rev: 24.4.2
In your docker-compose.yml / Dockerfile (module 01):
services:
db:
image: postgres:16
For each one, ask the same question: could this name, in theory, point at different content tomorrow without the reference itself changing?
Write your diagnosis down before continuing — for each reference, "immutable" or "mutable", and why.
What pre-commit.com actually documents
Here's the only precise, official source on this topic (pre-commit.com) — don't paraphrase it from memory:
"pre-commit assumes that the value of
revis an immutable ref (such as a tag or SHA) and will cache based on that."
And, further down, the explicit warning:
"Using a branch name (or
HEAD) for the value ofrevis not supported and will only represent the state of that mutable ref at the time of hook installation (and will NOT update automatically)."
What this means in practice: rev: v4.6.0 is treated by pre-commit as a fixed point — pre-commit caches the hook's environment based on that exact value. But a Git tag isn't cryptographically immutable: nothing technically stops a repository maintainer from moving an existing tag to point at a different commit (git tag -f). That's rare on well-maintained repos like pre-commit-hooks or black, but it isn't impossible — and that's exactly the gap a tag-based rev, unlike a SHA-based one, leaves open. A commit SHA, by contrast, is a hash of the content: it can't be "re-pointed" without changing value.
The worst case, explicitly documented, is rev: main or rev: HEAD: it's not "always the latest version" as you might assume — it's a snapshot frozen at the moment the hook was installed, which never updates on its own. That's the worst of both worlds: neither the freshness you'd expect, nor the immutability guarantee a SHA would give you.
The documented fix: autoupdate --freeze
pre-commit autoupdate --freeze
The official example:
Updating https://github.com/pre-commit/pre-commit-hooks ... updating v2.1.0 -> v2.4.0 (frozen).
Result in the file: rev becomes the SHA of the commit matching v2.4.0, with a comment preserving human readability:
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: a1b2c3d4e5f6... # frozen: v2.4.0
You get the guaranteed immutability of a SHA without losing the ability to tell, at a glance, which version it corresponds to.
Your before/after diff
Now produce, for real, the diff of your own .pre-commit-config.yaml:
pre-commit autoupdate --freeze
git diff .pre-commit-config.yaml
The expected result looks like:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: 2c9f875913ee60ca25ce70243dc24d5b6415598c # frozen: v4.6.0
hooks:
- id: trailing-whitespace
Reasoned extension: the other references
For the Docker images from module 01 (postgres:16) and the GitHub Actions from module 03 (actions/checkout@v4), the same underlying principle applies — a mutable tag can, in theory, end up pointing at different content — but pre-commit.com does not document this risk for those surfaces, this is the same reasoning applied by analogy. Note that clearly in your audit:
postgres:16is a minor tag: it points at the latest published16.ximage, and its content changes with every patch release — that's intentional behavior, not a bug, but it's still a mutable tag. A stricter pin would bepostgres:16.4(still mutable in theory, but changes less often) or a digest,postgres@sha256:...(immutable, guaranteed by the registry).actions/checkout@v4is a mutable tag in the same way an unfrozen pre-commit tag is — GitHub allows action-repo maintainers to re-point major-version tags. A stricter pin would beactions/checkout@<full sha>.
Self-assessment checklist
Compare your audit against these points:
- For each reference, did you distinguish "guaranteed immutable" (SHA/digest) from "mutable by convention but rarely changed in practice" (versioned tag) from "mutable and not pinned at all" (
main,latest,HEAD)? - Did you cite pre-commit.com's exact wording to justify the risk on
rev, rather than an approximate paraphrase? - Does your before/after diff show a real SHA, obtained from an actual run of
pre-commit autoupdate --freeze, not a made-up example? - Did you explicitly flag the "Docker images / GitHub Actions" section as a reasoned extension, not a guarantee documented by the same source as
rev? - Does your risk explanation name a concrete scenario (a compromised maintainer re-pointing an existing tag to malicious code, for instance) rather than a generality like "it's more secure"?
What's next
The next module shifts gears: your setup works, is mirrored in CI, and is audited on the supply-chain front — what's left is the raw performance of the container itself.
Check your understanding
According to pre-commit's official documentation, what does pre-commit assume about the value of the rev field in .pre-commit-config.yaml?
What happens, per the official docs, if you set a branch name (or HEAD) as the value of rev?
What does pre-commit autoupdate --freeze actually do?
Want to hear about the next modules?
The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.