Pin every GitHub Action to a SHA. One command.
Pinned actions are step one. Step two is the rest of the workflow.
The Cut Your CI Bill cookbook covers permissions, concurrency, timeouts, cache, and matrix strategy in 30 paste-ready patterns. $19, one-time, MIT-licensed templates.
Get the cookbook
Tags are mutable. Branches are mutable. The author of an action you use
can rewrite v4 tomorrow morning to point at code that
exfiltrates your GITHUB_TOKEN. The first you'd hear about it
is when an attacker pushes commits to your default branch using the
privileges your CI handed out the last time the workflow ran.
This is not theoretical. The tj-actions/changed-files compromise in early 2025 hit thousands of repos this exact way. Affected workflows pinned to a tag. The patched workflows pinned to a SHA.
GitHub's own security hardening guide says, plainly: pin third-party actions to a full-length commit SHA.
Nobody does this by hand
A 40-character SHA is not a thing humans want to type into YAML. So almost everyone uses tags. And the few people who pin to SHAs do it for one or two actions in one or two repos and then give up.
pin-actions
is a free MIT CLI that walks every .github/workflows/*.yml,
finds every uses: owner/repo@ref line, resolves the ref
against the GitHub API, and rewrites the line in place to use the full
SHA - keeping the original ref as a comment so the file is still readable.
Before:
- uses: actions/checkout@v4
After:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4
One command
npx pin-actions
That's the whole flow. It walks your workflows, calls the GitHub API to resolve each ref, rewrites the file, and prints what it changed.
In CI, use --check to fail the job if anything is unpinned:
npx pin-actions --check
That's a one-line, zero-config supply-chain CI gate.
What it leaves alone
- Local actions (
uses: ./actions/foo) - they live in your repo. - Docker actions (
uses: docker://alpine:3) - they're versioned at the registry, not at git. - Already-pinned refs (40-char SHA) - it's idempotent. Run it as often as you want.
Rate limits
Unauthenticated GitHub API requests are limited to 60 per hour. Most
workflows have under ten unique uses: lines, so the
unauthenticated limit is fine for one-off runs. For CI, pass a token:
GITHUB_TOKEN=ghp_xxx npx pin-actions --check
In a GitHub Actions workflow, ${{ github.token }} is the
token to pass.
The companion tool
ci-doctor
flags unpinned actions as a warning and points at pin-actions.
The two-line workflow hardening pass is:
npx ci-doctor --fix # adds permissions, concurrency, timeouts
npx pin-actions # pins every action to a SHA
Both finish in under two seconds on a normal repo. Both are MIT.
Try it
npx pin-actions
Source on GitHub.