Pinned GitHub Action (SHA vs tag)
A pinned GitHub Action references a specific commit SHA rather than a mutable tag (like v3) or branch. Pinning to a SHA means the action's source code can't change underneath you — a key supply-chain defense.
The problem with tag and branch refs
GitHub Actions can be referenced three ways in a workflow YAML's `uses:` field: by tag (`actions/checkout@v4`), by branch (`actions/checkout@main`), or by commit SHA (`actions/checkout@8e5e7e5...`).
Tags and branches are MUTABLE. The action maintainer (or anyone with write access) can re-point a tag or branch to a different commit at any time. Today's `actions/checkout@v4` is whatever the v4 tag currently points to; the bytes can change tomorrow without any version-number warning.
Commit SHAs are IMMUTABLE. `actions/checkout@8e5e7e5d6b48f1abe9d11d1...` will always be exactly the same code, forever. Pin to SHA and the action becomes content-addressed.
Why this matters — the threat model
Threat 1: Maintainer compromise. An attacker phishes or credentials-stuffs a popular action's maintainer account. Pushes a malicious version. Retags v4 to the malicious commit. Every workflow using `actions/checkout@v4` runs the malicious code on the next run.
Threat 2: Intentional malicious release. The maintainer themselves goes bad — pushes a malicious version under an existing version tag (rare but documented).
Threat 3: GitHub-level compromise. An attacker compromises GitHub infrastructure and modifies tag refs. Extremely rare but covered by this defense.
All three threats reduce to 'the bytes behind a tag/branch ref are not what they were when you originally added the action.' Pinning to SHA defeats all three.
The trade-off — and how to handle it
Pinning to SHA means you don't get automatic security updates. If the action maintainer ships a patch for a real vulnerability, your workflow keeps using the old (vulnerable) SHA until you manually update.
Standard mitigation: use Dependabot (or Renovate) to monitor action versions and open PRs to bump SHA when new releases ship. The bot reads the action's release feed and proposes a PR with the new SHA. Maintainer reviews + merges.
This converts the upgrade decision into a controlled, reviewed PR rather than an implicit silent upgrade on every workflow run. Best of both worlds: immutability of pinned SHAs + visibility into when upgrades happen.
Detection — static rule that flags non-SHA refs
Static rule: any `uses:` line that doesn't end in a 40-character hex SHA gets flagged. The rule is regex-trivial; the noisy part is exception handling for cases where the team has consciously chosen not to pin (rare for production workflows).
LGTM Security's rule id: workflow.action-pinned-to-mutable-ref. Severity is configurable per-rule — most teams set it to 'warn' (non-blocking PR comment) rather than 'block' because the false-positive rate on early-stage repos is high.
Similar checks in: OSSF Scorecard (Pinned-Dependencies), GitHub's actions analysis (deprecation warnings), CodeQL (custom queries).
When pinning to SHA is OVERKILL — and when it's table stakes
Pinning is table stakes when:
(1) You publish a package (npm/PyPI/RubyGems). Your release pipeline IS the supply chain for your users.
(2) Your workflow has access to production secrets. A compromised action with secrets in env is a full incident.
(3) You're a public repo with many contributors. Surface area for action-source tampering is larger.
Pinning is overkill when:
(1) Solo throwaway side project, no secrets, no deployment. Tag refs are fine.
(2) Cost of automation maintenance exceeds risk reduction (rare but possible for very small teams).
Most professional teams default to SHA pinning + Dependabot — the maintenance cost is minor (one PR per action update per month-ish) and the risk reduction is real.
Examples
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/cache@v3
# ❌ All three depend on v4/v3 not being retargeted by attackerssteps:
- uses: actions/checkout@8e5e7e5d6b48f1abe9d11d162cffe11abe611ac5
- uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a
- uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
# ✓ Bytes immutable; Dependabot PRs propose new SHAs when releases shipEnable LGTM's pinned-action detector
Flags any uses: without 40-char SHA · per-rule severity · audit log
Go to the product pageFAQs
How do I find a commit SHA for an action?
Visit the action's GitHub release page (e.g., github.com/actions/checkout/releases/tag/v4.1.0). The release header shows the SHA. Copy + paste into your workflow. Tools like `actionlint` and Dependabot can compute these for you automatically.
Does pinning slow me down?
Slightly. You don't get automatic security updates; you wait for Dependabot/Renovate to open a PR. The latency is typically days, not weeks — bots check for new releases hourly. The trade-off is visibility and control over what's running in your CI.
Are GitHub-published actions (actions/checkout, etc.) safe enough to not pin?
GitHub's first-party actions have stronger maintainer controls but aren't immune to the threat model. The 'Pinned-Dependencies' score in OSSF Scorecard applies regardless of who maintains the action. Treat actions uniformly — pin everything to SHA.
What about reusable workflows (callable workflows)?
Same model. Reusable workflows referenced by `uses: owner/repo/.github/workflows/wf.yml@ref` should pin the ref to a commit SHA, not a branch or tag. Same threat model applies.
Can I auto-pin in bulk across a monorepo?
Yes — tools like `pin-github-action` (npm) or `ratchet` (Go) walk your workflows and replace tag/branch refs with the current SHA. Run once to pin, run periodically (or via Dependabot) to update. Most teams adopt SHA pinning incrementally — pin the most-sensitive workflows first, expand as the team gets comfortable.
Related across LGTM
Related terms
Self-hosted runner abuse
Self-hosted runner abuse: an attacker forks a public repo using a self-hosted GitHub Actions runner, opens a PR, and gets remote code execution on the runner's host — often the maintainer's own infrastructure.
pull_request_target attack
A pull_request_target attack abuses GitHub Actions workflows that combine the pull_request_target trigger (runs with base-repo secrets) with checking out fork-controlled code, giving an attacker's fork PR access to your secrets.
Software supply-chain attack
A software supply-chain attack compromises a software product by attacking something earlier in the build/distribute chain: a dependency, a build tool, a package registry, a CI/CD pipeline, or a maintainer account. The downstream consumer ships compromised code without knowing.
CI/CD security
CI/CD security covers the attack surface introduced by your build and deploy pipelines: GitHub Actions workflows, Dockerfiles, IaC configs, dependency management, and secret handling. Distinct from (and complementary to) application-layer AppSec.