Pinfix: pin GitHub Actions and Docker images in one step
A tag is a moving label. uses: actions/checkout@v4 and FROM node:22 run whatever their owner points the label at today, and in March 2025 a popular action's tags were moved to malicious code. A full commit SHA or an image digest cannot be moved. GitHub can now require SHA pinning for a whole organisation, so unpinned lines fail the workflow.
Pinfix finds those lines and rewrites them, keeping the version readable:
Dockerfile
line 1
- node:22-alpine
+ node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
line 4
- ghcr.io/astral-sh/uv:latest
+ ghcr.io/astral-sh/uv:latest@sha256:4a1f3666816dafadd20ceb71eac9a7d56d7ef30279f7d64048519ab901ada336
line 5
- mcr.microsoft.com/dotnet/runtime:8.0
+ mcr.microsoft.com/dotnet/runtime:8.0@sha256:60d11c2be381e7f90b8bd984472dd2432933858aba55087d5936980cf2071d5a
3 references would change. Run again with --write to save.
That is real output from running pinfix pin on a three-line Dockerfile (digests are what the registries returned when it was made; tags move, which is the point). For actions the result looks like this:
- uses: actions/checkout@v4
+ uses: actions/checkout@<full 40-character commit SHA> # v4.4.0
In VS Code
Install it from the Extensions view (search for Pinfix, id plainwork.pinfix). Open a workflow, a Dockerfile, a compose file or a Kubernetes manifest. Unpinned references are underlined; press Ctrl+. (Cmd+. on a Mac) on one and choose Pin to a full commit SHA or Pin to a sha256 digest. Or run Pinfix: Pin every reference in this file from the Command Palette.
Underlining uses only the open file and needs no network. The network is used only when you choose a quick fix or a command, and only for the exact references you are pinning.
GitHub allows 60 unauthenticated requests an hour. If you are signed in to GitHub in VS Code, Pinfix uses that session (it never asks you to sign in); otherwise it reads GITHUB_TOKEN.
On the command line
npx @plainworkdigital/pinfix check
npx @plainworkdigital/pinfix pin .github/workflows/ci.yml
npx @plainworkdigital/pinfix pin .github/workflows/ci.yml --write
check needs no network and exits 1 when something is unpinned, so it works as a CI step. pin prints what it would change and writes only with --write. If any reference cannot be resolved, --write saves nothing (add --partial to save the ones that did resolve); files are replaced atomically. Set GITHUB_TOKEN to raise GitHub's rate limit.
Options: --actions or --images (only that kind), --json. Outside workflow files only image: is read (a container: main key or image: logo.png in a Jekyll or mkdocs file is ignored).
What it handles
| Where |
What gets pinned |
.github/workflows/*.yml, action.yml |
uses: owner/repo@ref, owner/repo/path@ref, reusable workflows, docker://image |
| Dockerfile, Containerfile |
FROM image[:tag] (not scratch, earlier stages or $VARIABLES) |
Compose, Kubernetes, workflow services: / container: |
image: name[:tag] |
How it resolves: a tag or branch becomes a commit through GitHub's "Get a commit" API (annotated tags resolve to their commit); the version comment is the most specific tag on that commit (v4.4.0 over v4). An image tag becomes the digest the registry reports for that tag; for multi-architecture images that is the index digest, so every platform still works. Registries are asked anonymously.
Pro ($9, one-time key)
Pinning one reference or one file is free and stays free. Pro is for whole repositories and teams:
- Pin a folder.
pinfix pin . --write, or Pin every reference in the workspace in VS Code.
- Verify.
pinfix verify checks that each version comment still resolves to the pinned SHA (a stale comment or a moved tag shows up as drift) and that each pinned image digest is still what its tag points at.
- Update.
pinfix update --write moves pins to the newest release, staying in the same major version unless you pass --major; image digests are refreshed for the same tag.
- Policy and SARIF. A policy file lists trusted owners and ignore patterns (
--policy file.json, or the pinfix.policyFile setting); --format sarif writes SARIF 2.1.0 for code scanning.
Get a key at https://plainworkdigital.com/d20261016a4/ , then run pinfix activate <key> or Pinfix: Enter Pro key in VS Code. The key is checked with one request to Plainwork's licence server and remembered for 10 minutes; offline, the free features keep working.
Privacy
No telemetry. File contents are never sent anywhere: only owner/repo and a tag, or an image name and tag, go to api.github.com or the image's own registry, when you ask for a pin. A repository cannot make Pinfix contact a server on its own: nothing is looked up until you act.
What it cannot do
- Private repositories and private images (no credentials are used for registries; GitHub uses your token if you provide one).
- GitHub Enterprise Server, and registries that need a login.
- References built from variables (
FROM $BASE, uses: ${{ ... }}), Helm or Terraform templates: these are skipped and counted, not guessed.
- A pin proves you run the same bytes tomorrow, not that those bytes are safe. Pinning does not review the action's code.
- A tag that is moved before you pin it is pinned as moved. Pin from a tag you have already reviewed.