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 works

AI 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

Pinned to an immutable SHA (real SHAs, copy them)
# 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.

Mutable tags and branches
steps:
  - uses: actions/checkout@v7        # tag, mutable, can be re-pointed
  - uses: some-org/some-action@main  # branch, anyone with push can change it

Seen 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:
Every one of the 90 external action references in this repository is pinned to a full commit SHA. The version tag survives as a comment.
      - 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
All 73 of them, third-party actions included. A security scanner holding itself to the advice it gives everyone else.
      - 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.0
Every action reference in this file is pinned to a full SHA. Elsewhere in it, that includes tj-actions/verify-changed-files -- from the org whose changed-files action was hijacked through a moving tag (CVE-2025-30066).

A 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.

Prompt for your coding agent
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?

  1. Scan for unpinned references: look for uses: lines ending in @v1/@v2/@main/@master rather than a 40-char hex SHA.

  2. 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 with gh api repos/<owner>/<action>/git/tags/<sha> --jq .object.sha.

  3. Convert them in one shot with a SHA-pinning rewrite tool, or resolve each tag yourself and preserve the version as a trailing comment.

  4. 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.

  1. Fix this one thing

    Copy the prompt above

    Hand P5.1 to your coding agent and fix it in your repo today.

  2. 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.

  3. 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.

All GitHub Actions best practices

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.

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