| name | pin-images |
| description | Audit and refresh the digest pins for the Docker base images referenced by `FROM` in `docker/*/Dockerfile`, with a supply-chain cooldown that refuses freshly-rebuilt digests. The image tag (`golang:1.26.5-alpine`) is the version source of truth and is owned by `go-upgrade` / `tools-upgrade` / `make sync-versions` โ this skill NEVER changes tags, only the trailing `@sha256:...` digest. `docker/images-pin.toml` is the resolved `image:tag โ digest` lockfile (SSOT), driven by `make pin-images-resolve` / `pin-images-apply` / `pin-images-check` (backed by `scripts/pin-images`). `resolve` re-resolves each tag's current digest via `docker buildx imagetools inspect` and quarantines any whose image-config `created` is younger than `PIN_IMAGES_MIN_AGE_DAYS` (default 14; pass `days=N` to override, `0` disables). Because a mutable tag has no queryable history, the step-back target is the tool's own prior lock entry; on bootstrap a still-fresh image is left tag-only and reported with its re-run date. Verifies with `make pin-images-check` + `make docker-lint` (hadolint). Sibling of `actions-pin` (which pins GitHub Actions `uses:`). Use on a routine cadence, after a base-image / registry security advisory, or to pin an image that was previously quarantined once it has aged. |
Docker Base Image Pin Refresh
This skill audits and refreshes the digest pins of the FROM base images in docker/*/Dockerfile, with a supply-chain cooldown gate: a digest whose image-config created timestamp is newer than the exclusion window (PIN_IMAGES_MIN_AGE_DAYS, default 14) is never adopted. A freshly-published (possibly compromised) rebuild is thus never pulled in before upstream has time to detect and revoke it.
It is the sibling of actions-pin โ that skill pins GitHub Actions uses: to commit SHAs; this one pins Docker base images to digests. They share the same cooldown philosophy but operate on different SSOTs.
The tag is not this skill's to change. golang:1.26.5-alpine, node:24.18.0-alpine, etc. are version-pinned by make sync-versions (from mise.toml) and bumped by go-upgrade / tools-upgrade. This skill only manages the @sha256:... digest that follows the tag. Never edit a tag here โ if a version bump is needed, stop and defer to the owning skill.
A Japanese reference translation is available at SKILL.ja.md in the same directory (not loaded as a skill; for human reference only).
How Pinning Works in This Repo
Read this before doing anything โ the mechanism determines every step below. Read the actual scripts/pin-images/main.go and .makefiles/docker/pin.mk at runtime; this section is the summary, the code is the source of truth.
- Each base image is pinned as
FROM image:tag@sha256:<hex> [AS <stage>]. The tag is the version source of truth; the @sha256 digest is the immutability lock.
docker/images-pin.toml is the lockfile: "image:tag" = "sha256:<hex>" (SSOT for apply, regenerated by resolve).
make pin-images-resolve โ collects every FROM image:tag, resolves its current digest via docker buildx imagetools inspect, applies the cooldown, and rewrites the lockfile. Env PIN_IMAGES_MIN_AGE_DAYS (default 14) controls the gate.
make pin-images-apply โ normalizes each FROM to image:tag@<digest> from the lockfile. Quarantined images (absent from the lockfile) are normalized back to tag-only โ apply strips a stale digest, so the lockfile is the single source of truth.
make pin-images-check โ verifies FROM matches the lockfile without writing (CI / hook); network-free (reads lockfile + Dockerfiles only). The CI gate is .github/workflows/pin-images-check.yaml.
The Cooldown & Quarantine Rule (core of this skill)
For each image:tag (N = exclusion days, cutoff = now - N days):
- Aged โ adopt. If the current digest's image-config
created is older than the cutoff โ write it to the lockfile; apply pins it.
- Fresh, with a prior lock โ keep prior. If the current digest is inside the window but the lockfile already has an entry for this
image:tag โ keep the existing (older, already-vetted) pin, and report the quarantine.
- Fresh, no prior lock (bootstrap) โ tag-only. If the current digest is inside the window and there is no prior lock entry โ leave the image absent from the lockfile;
apply keeps it on the tag. Report it with the date it becomes adoptable (created + N days).
Why this differs from actions-pin: a Git tag/release has an enumerable, immutable history, so actions-pin can step back to an older exact version. A mutable image tag has no queryable history โ the registry only answers "what does this tag point to now." So the only step-back target is the tool's own prior lock entry (rule 2). On first-ever pin there is nothing to step back to, hence rule 3 leaves it tag-only rather than adopting a fresh (unvetted) digest.
Official images are rebuilt often (base-OS CVE patches), so a fresh current digest is common and quarantine is expected, not an error.
When to Use
- Routine periodic refresh of the base-image digest pins.
- After a base-image or registry supply-chain advisory.
- To pin an image that a previous run left quarantined, once it has aged past the window.
Do NOT use this skill for:
- Bumping an image version/tag (Go/node/python runtime) โ use
/go-upgrade or /tools-upgrade (+ make sync-versions).
- GitHub Actions
uses: pins โ use /actions-pin.
- Dockerfile lint findings โ use
make docker-lint.
Arguments
Parse the invocation arguments (order-independent):
| Token | Meaning | Default |
|---|
a bare integer, or days=N (or --days N) | Exclusion window in days = PIN_IMAGES_MIN_AGE_DAYS. | 14 |
Examples: /pin-images (14d) ยท /pin-images 30 (30d) ยท /pin-images days=7.
The exclusion days must be a non-negative integer. 0 disables the cooldown (adopt even brand-new digests) โ only honor it when the user explicitly passes 0, and surface the supply-chain risk.
AI Modification Scope
Per the "Exception: Skill Execution" clause in CLAUDE.md, the following paths may be modified while this skill runs (this skill touches the sensitive docker/ area โ that is intentional and scoped to pinning only):
docker/*/Dockerfile โ the @sha256:... digest on FROM lines (written by make pin-images-apply)
docker/images-pin.toml โ the lockfile (written by make pin-images-resolve)
The following remain protected even during skill execution:
AGENTS.md / CLAUDE.md
- Generated files (
**/*.gen.go, *.sql.go, *_mock.go, **/openapi.gen.yaml, generated content under docs/)
- Any file unrelated to the digest pin. Do NOT change a
FROM tag, RUN/COPY steps, or scripts/pin-images โ if a tag bump is needed, surface it and stop.
Execution Steps
0. Pre-flight: vendor consistency + registry access
make pin-images-* runs go run ./scripts/pin-images, which compiles against vendor/. vendor/ is gitignored, so a parallel checkout can leave it inconsistent with the current branch's go.mod, failing with vendor/modules.txt: ... inconsistent. If so, run go mod vendor once, then proceed.
resolve calls docker buildx imagetools inspect, which hits the registry. Docker Hub's anonymous pull rate limit returns 429 Too Many Requests after a burst. If resolve fails with 429, have the user authenticate (raises the limit) โ suggest they type ! docker login in the prompt โ then retry. apply / check are network-free and unaffected.
1. Parse Arguments and Inventory
Parse the arguments into <N> (exclusion days). Then:
- Read
docker/images-pin.toml for the current image:tag โ digest set.
- Grep
FROM across docker/*/Dockerfile to map each base image to its file locations and tag (note images referenced in multiple stages / files โ e.g. golang:*-alpine appears in several stages).
2. Resolve
make pin-images-resolve PIN_IMAGES_MIN_AGE_DAYS=<N>
resolve re-resolves every image:tag and prints โ ๏ธ ... ใฎใใๆขๅญใใณใ็ถญๆ (rule 2) or โ ๏ธ ... tag ใฎใพใพ skip (rule 3) for any inside the window โ expected, not a failure. Note each quarantined image and, for bootstrap skips, compute its adoptable date (created + N days) from the warning's age.
3. Apply
make pin-images-apply
Normalizes every FROM from the lockfile (aged images gain / refresh @sha256:...; quarantined images are stripped back to tag-only).
4. Verify
make pin-images-check
make docker-lint
Report OK / FAIL per command. Do NOT auto-roll-back on failure โ the user decides.
5. Final Report
Summarize in Japanese: images newly pinned / digest-refreshed (with the digest age), images quarantined (rule 2 kept-prior / rule 3 tag-only, each with reason and โ for tag-only โ the adoptable date), and verification result. List quarantined images so the user knows to re-run this skill once they age. Do NOT commit, stage, or push โ the user runs /commit (these changes are CI:- or Build:-prefixed) manually.
Notes
- The tag is off-limits. This skill only moves digests. A version bump is
go-upgrade / tools-upgrade + make sync-versions.
- Quarantine is normal, not a failure. Official base images rebuild frequently, so a fresh current digest is common; the gate keeps the prior aged pin (or tag-only on bootstrap) rather than adopting it.
- A fast-moving tag can stall. If a tag is rebuilt more often than every
N days, the current digest is always fresh and the pin never advances past the last aged one. That is the accepted cooldown cost; lower N deliberately (and note the risk) only if a security patch must be adopted sooner.
- Multi-arch digest.
resolve pins the top-level image-index digest (Docker resolves the per-platform manifest from it), and reads the earliest per-platform created for the age check (most conservative).
check is network-free. It reads the lockfile + Dockerfiles only, so it is safe as a CI / pre-commit gate even without registry access.
- Enforcement. The local gate is the lefthook
pin-images pre-commit hook (glob-filtered to docker/**/Dockerfile + docker/images-pin.toml) plus the pin-images-check.yaml CI workflow โ both mirror pin-actions. Escalating to a required status check (a hard merge block via the branch ruleset) is intentionally left to the downstream template consumer: the boilerplate ships the check, not the mandate. Note a path-filtered workflow made a required check blocks PRs that do not touch those paths, so that escalation also needs an always-run adjustment โ another reason it is deferred to the consumer.
- Idempotency: a second
apply is a no-op and pin-images-check passes.
- The skill never auto-pushes.
Checklist
Confirm before reporting completion: