Problem
The hadolint-docker pre-commit hook was reddening main and every open PR (via the pre-commit --all-files CI job, which lints the whole merge ref, not just a PR's diff), including PRs that touch zero Docker files (e.g. #1658).
Root cause: version drift, not new Dockerfile debt
.pre-commit-config.yaml pins rev: v2.14.0 on the hadolint/hadolint repo, but that pin only fixes which commit of hadolint's hook definition (.pre-commit-hooks.yaml) is used. That file declares:
- id: hadolint-docker
entry: ghcr.io/hadolint/hadolint hadolint
No image tag. Docker resolves an untagged reference to :latest, so the actual linter binary floats independently of our rev: pin. Confirmed:
$ docker run --rm ghcr.io/hadolint/hadolint:v2.14.0 hadolint --version
Haskell Dockerfile Linter 2.14.0
$ docker run --rm ghcr.io/hadolint/hadolint:latest hadolint --version
Haskell Dockerfile Linter 2.15.1
Running both versions against the unchanged Dockerfiles (with the repo's .hadolint.yaml ignore list applied, matching what pre-commit actually does):
v2.14.0 (pinned rev): exit 0, no findings
latest / 2.15.1: exit 1
Dockerfile:149 DL3066 Non-numeric user-id may not be resolvable by host system
Dockerfile:165 DL3025 Use arguments JSON notation for CMD and ENTRYPOINT arguments
Dockerfile.dev:62 DL3066 Non-numeric user-id may not be resolvable by host system
Dockerfile.test:21 DL3066 Non-numeric user-id may not be resolvable by host system
hadolint 2.15.x applies DL3066 to any USER <name> directive (all four are pre-existing, intentional USER cudly/devuser/e2e non-root switches after adduser/addgroup) and now also applies DL3025 to HEALTHCHECK's CMD sub-clause (Dockerfile:165 is the HEALTHCHECK line, not the container's ENTRYPOINT/CMD, which are already in exec/JSON form). Neither Dockerfile line changed; the git history on all three Dockerfiles predates today, and .pre-commit-config.yaml's hadolint rev: hasn't moved since 2026-02-20. Only the untagged upstream image moved.
Fix
Replace the external hadolint-docker hook with a local hook definition pinned to the immutable digest of the same v2.14.0 image (ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e), so the linter version can only change via an explicit bump in this repo, not silently via an upstream :latest retag. No Dockerfile changes were needed or made.
Blast radius
Blocked every open PR's Run pre-commit hooks CI job (whole-merge-ref lint), regardless of whether the PR touched Docker files.
Problem
The
hadolint-dockerpre-commit hook was reddeningmainand every open PR (via thepre-commit --all-filesCI job, which lints the whole merge ref, not just a PR's diff), including PRs that touch zero Docker files (e.g. #1658).Root cause: version drift, not new Dockerfile debt
.pre-commit-config.yamlpinsrev: v2.14.0on thehadolint/hadolintrepo, but that pin only fixes which commit of hadolint's hook definition (.pre-commit-hooks.yaml) is used. That file declares:No image tag. Docker resolves an untagged reference to
:latest, so the actual linter binary floats independently of ourrev:pin. Confirmed:Running both versions against the unchanged Dockerfiles (with the repo's
.hadolint.yamlignore list applied, matching what pre-commit actually does):hadolint 2.15.x applies DL3066 to any
USER <name>directive (all four are pre-existing, intentionalUSER cudly/devuser/e2enon-root switches afteradduser/addgroup) and now also applies DL3025 toHEALTHCHECK'sCMDsub-clause (Dockerfile:165is theHEALTHCHECKline, not the container'sENTRYPOINT/CMD, which are already in exec/JSON form). Neither Dockerfile line changed; the git history on all three Dockerfiles predates today, and.pre-commit-config.yaml's hadolintrev:hasn't moved since 2026-02-20. Only the untagged upstream image moved.Fix
Replace the external
hadolint-dockerhook with a local hook definition pinned to the immutable digest of the samev2.14.0image (ghcr.io/hadolint/hadolint:v2.14.0@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e), so the linter version can only change via an explicit bump in this repo, not silently via an upstream:latestretag. No Dockerfile changes were needed or made.Blast radius
Blocked every open PR's
Run pre-commit hooksCI job (whole-merge-ref lint), regardless of whether the PR touched Docker files.