Pin GitHub Actions to commit SHAs
Reference every third-party action by its full 40-character commit SHA (with the version as a trailing comment), not a mutable `@v4` tag or `@main` branch that its maintainer, or an attacker, can re-point.
How StarSling worksAI agents open the PR
StarSling agents inspect your workflow, apply this optimization, and open a reviewable PR automatically.
Do this
Third-party actions are pinned to an immutable 40-character commit SHA, with the human-readable version kept as a trailing comment so Dependabot and Renovate still track upgrades. A compromised upstream tag can no longer silently change the code that runs under your workflow's secrets and GITHUB_TOKEN.1
# The actions people pin most, at the full-length commit SHA of a release, with
# the version kept as a trailing comment. Resolved from upstream on 2026-07-14:
# gh api repos/actions/checkout/git/ref/tags/v7.0.0 --jq .object.sha
# Every SHA below runs on node24. GitHub's runners have defaulted to Node24 since
# 2026-06-16 and drop node20 in the fall of 2026, so the previous majors
# (checkout v4, setup-node v4, cache v4) warn today and will fail later.
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# A node24 action needs Actions Runner >= 2.327.1, which matters only if you
# host your own runners. Pin the versions you actually run: re-derive any SHA
# with the command above, then preserve the version as a trailing comment.Avoid this
A re-pointed tag runs new code under your secrets and GITHUB_TOKEN on the next CI run.
steps:
- uses: actions/checkout@v7 # tag, mutable, can be re-pointed
- uses: some-org/some-action@main # branch, anyone with push can change itSeen in the wild
Infisical, Trivy, and Immich pin actions to immutable commits so a moving tag cannot change what CI executes.
- name: ☁️ Checkout source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 🔧 Setup Node 22
id: setup-node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with: - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run Trivy vulnerability scanner and create GitHub issues
uses: knqyf263/trivy-issue-action@4466f52d1401b66dd2a2ab9e0c40cddc021829ec # v0.0.6 - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0A SHA pin can still lie: impostor commits
A 40-character pin removes the mutable ref, but it proves nothing about where the commit came from. GitHub keeps a repository and all of its forks in one shared object pool, so uses: owner/action@<sha> resolves even when that commit exists only on an attacker's fork and was never merged into the canonical repository: the pin passes review looking immutable while pointing at attacker-controlled code. Chainguard's impostor-commit research documented the class, and the tj-actions incident executed exactly such a commit: one that belonged to no branch, run because a ref pointed at it. So verify reachability, not just format: git branch --contains <sha> in a bare clone of the canonical repo must name at least one branch, or run zizmor's impostor-commit audit in CI to check every pin automatically. A commit reachable only from a tag is not automatically an impostor, but it is not automatically safe either: a legitimate release commit can sit on a tag and no branch, while a re-pointed tag is exactly how the tj-actions payload ran, so name the tag with git tag --contains <sha> and trust the pin only if that tag is the release the upstream itself published. Impostor action SHAs are one of the ten attack vectors the ci-secure skill scans workflows for.
Runs on your machine. GitHub API optional, for the impostor-SHA check and the dormancy note.
Why it matters
A tag like @v4 is a moving pointer. If the action's maintainer account is compromised, the attacker re-points the tag at malicious code and your next CI run executes it with full access to your secrets. This is not hypothetical, the tj-actions/changed-files compromise (March 2025, CVE-2025-30066) worked exactly this way. Pinning to a SHA makes the code immutable; a branch ref like @main is the worst case and should be pinned or vendored. The recommended docker/build-push-action config ships SHA-pinned for this reason.
When to use
Use it when
Every third-party action reference. First-party actions/* are lower-risk but still worth pinning for a consistent policy.
Be careful when
Pinning has no real downside beyond upgrade friction, which Dependabot/Renovate handle via the version comment. For an action that only ships a main branch, vendor it (fork and pin your own SHA) or push upstream to cut releases.
Verify on your repo
Hand this prompt to your coding agent (Claude Code, Cursor, and the like) to audit and fix this practice in your own repo.
Inspect this repo's .github/workflows for third-party actions referenced by a mutable ref: `uses:` lines ending in `@v1` / `@v4` / `@main` / `@master` rather than a full 40-character commit SHA. For each, pin it to the immutable 40-char SHA of the release currently in use and keep the human-readable version as a trailing comment (`@<sha> # v7.0.0`) so Dependabot and Renovate still track upgrades. Show me the diff and open a PR rather than applying it blindly.Prefer to check by hand?
Scan for unpinned references: look for
uses:lines ending in@v1/@v2/@main/@masterrather than a 40-char hex SHA.Resolve the SHA for any tag yourself, so you can check the ones above (or pin a version we don't list):
gh api repos/actions/checkout/git/ref/tags/v7.0.0 --jq .object.sha. If the tag is annotated, that returns a tag object, so deref it withgh api repos/<owner>/<action>/git/tags/<sha> --jq .object.sha.Convert them in one shot with a SHA-pinning rewrite tool, or resolve each tag yourself and preserve the version as a trailing comment.
For stronger enforcement, run a workflow-security audit in CI that catches tag-pinned actions and SHAs that don't exist in the action's history.
Go further
One fix, all of them, or forever.
You have the prompt for this one practice. Here is how much further you can take it, each step doing more for you than the last.
Fix this one thing
Copy the prompt above
Hand P5.1 to your coding agent and fix it in your repo today.
Fix everything, once
Install the ci-score skill
One prompt grades your whole workflow config against all 11 CI Score checks, this one included, and hands your agent a ranked fix for every gap it finds. Open source, runs locally. It grades configuration, not speed.
Keep it fixed, forever
Install the StarSling GitHub App
Connect GitHub and the fixes stay applied as your CI evolves, with agents that keep inspecting your workflows and opening optimization PRs you review.
More best practices for GitHub Actions
Where to go next in the CI best-practices catalog.
- Security
Scope id-token: write to the publishing job
In GitHub Actions, declare
id-token: writein the specific publishing job'spermissions:block, never at the workflow's top level, so an OIDC publish token isn't minted for jobs that run untrusted code. - Security
GitHub Actions permissions: use least privilege
Set a read-only top-level
permissions:default in every GitHub Actions workflow, then grant each job only the additionalGITHUB_TOKENscopes it needs, so unrelated steps cannot inherit repository write access. - Security
Prevent GitHub Actions cache poisoning
Keep untrusted code from writing cache entries that trusted release or deploy jobs restore, and cache inert dependency data rather than executable workspace state or credentials.
More ways to improve GitHub Actions
If you do not know why a run is slow yet, start with the diagnostic guide. Let an agent find which of these applies: /ci-speedup. If your workflows are already optimized but still slow, see our guide to fast GitHub Actions and GitHub Actions runner alternatives. Building containers in CI? The Docker workflow guide covers layer caching end to end. Want to see how your configuration measures up before changing anything? CI Score grades workflow config against a pass/fail rubric of these practices. It is not a speed measurement. To find exploitable workflow paths before attackers do, ci-secure checks the ten critical GitHub Actions attack vectors and lets you choose which fixes to apply.
- GitHub Actions too slow
- Fast GitHub Actions
- GitHub Actions alternatives
- GitHub Actions pricing
- Self-hosted GitHub Actions runners
- Docker CI on GitHub Actions
- Install the free /ci-score skill to improve your GitHub Actions setup
- Install the /ci-secure skill to close critical attack vectors in GitHub Actions
- Browse GitHub Actions agent skills
FAQ
Do I need to pin first-party actions like actions/checkout?
The immediate risk is highest for third-party actions, but pinning everything gives you one consistent, auditable policy. Pinning first-party actions too is the safer default.
Won't pinning to a SHA break my automated updates?
No, keep the version as a trailing comment (@<sha> # v7.0.0). Dependabot and Renovate read that comment and still open upgrade PRs, so you get immutability and updates.
Sources
1GitHub · security hardening (pin actions to a full-length SHA) (opens in new tab)
Last updated 2026-08-19