DevSecOps

How to Pin GitHub Actions to SHAs Correctly

A hands-on guide to pinning every third-party GitHub Action to a full commit SHA, automating updates with Dependabot, and avoiding the common pitfalls.

Shadab Khan
Security Engineer
4 min read

Tag-pinned GitHub Actions are a supply chain bomb waiting to go off. @v3 and even @v3.1.0 are mutable references that the action author can rewrite at any time, and the 2022 tj-actions/changed-files and 2024 reviewdog/action-* incidents both abused exactly that. This tutorial shows you how to convert every third-party action in your repo to a full 40-character commit SHA, keep them updated safely with Dependabot, and add a pre-merge check that rejects any unpinned action. Prerequisites: gh CLI 2.30+, write access to your repo, and 20–40 minutes depending on how many workflows you maintain.

Why is tag pinning unsafe?

Tags are branch pointers — they can move. A malicious maintainer or an account takeover can re-push v3 to a commit that steals your secrets. A full commit SHA is immutable content-addressed, so a compromised tag cannot poison your build unless the attacker finds a SHA-1 collision.

Only GitHub-owned actions under actions/* are relatively safe to tag-pin because they are covered by GitHub's own supply chain controls. Everything else — including 50k-star actions from well-known maintainers — should be SHA-pinned.

How do I find the right SHA?

Use gh api to resolve a tag to its commit SHA, or click the tag in the GitHub UI and copy the full SHA from the URL. Never trust a short SHA: always use the full 40 characters.

gh api repos/sigstore/cosign-installer/git/refs/tags/v3.5.0 \
  --jq '.object.sha'
# 59acb6260d9c0ba8f4a2f9d9af48c2b6d2a2a1e0

If the tag resolves to an annotated tag object rather than a commit, dereference it with the ^{} suffix or use git rev-parse. The GitHub API returns object.type: "tag" in that case — you need one more hop to reach the commit.

How do I convert my workflows?

Replace each uses: line with owner/repo@<40-char-sha> and add a comment with the human-readable version. The comment is what Dependabot updates alongside the SHA.

# before
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3

# after
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0

The trailing comment is not cosmetic — Dependabot parses it to decide which SHA to upgrade to next. If you omit the comment, Dependabot leaves the SHA frozen forever. That is actually acceptable for some orgs; just be deliberate about the choice.

How do I configure Dependabot?

Add a github-actions ecosystem entry in .github/dependabot.yml that groups Action updates and runs weekly. Dependabot automatically preserves SHA pinning when it opens PRs.

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      actions:
        patterns: ["*"]
    open-pull-requests-limit: 5

Grouping all Action updates into one PR per week is the right default for small teams — otherwise you drown in 15 PRs the day a popular action releases. Review the PR, run the full CI suite, and merge.

How do I enforce pinning in CI?

Run the zgosalvez/github-actions-ensure-sha-pinned-actions action in a PR check, itself pinned to a SHA. It fails the build if any workflow file references a tag instead of a SHA.

name: Enforce SHA pinning
on: [pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
      - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@fc87bb5b5a97953d987372e74478de634726b3e5
        with:
          allowlist: |
            actions/
            github/

The allowlist lets you exempt first-party GitHub actions. Add your own org prefix if you maintain internal shared actions — those live under your control so tag pinning is acceptable.

How do I audit historical workflow runs?

Use actionlint plus a one-liner grep to surface any remaining tag references, then use gh run list to see which workflows executed them recently. This gives you a risk-ordered cleanup backlog.

actionlint -color
grep -rEn "uses: [^@]+@v[0-9]" .github/workflows/
# .github/workflows/deploy.yml:14:      uses: actions/setup-node@v4
# .github/workflows/test.yml:22:       uses: codecov/codecov-action@v3

Fix the most-run workflows first — they have the highest blast radius. A daily cron workflow using a tag-pinned action is a much bigger risk than one that runs only on release.

How Safeguard Helps

Safeguard scans your .github/workflows/ directory as part of repository onboarding and flags every unpinned or tag-pinned third-party action as a supply chain finding. Griffin AI correlates the action with known compromised-package intelligence from the OpenSSF and npm advisories, so you know if a pinned SHA lives downstream of a recent incident. The platform generates an SBOM of your CI pipeline — actions, container images, and inline scripts — giving you a second chain of custody alongside your application SBOM. Policy gates can block a PR merge when a workflow adds a new unpinned action or when a pinned SHA is known-malicious, and violations route to Slack or Jira automatically. Keep your CI as trusted as your production code path.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.