---
title: "Pin GitHub Actions to a SHA: supply-chain hardening"
description: "Referencing actions by @v4 or @main lets a compromised tag run new code in your CI. Pin actions to a commit SHA in GitHub Actions, with copyable YAML."
url: https://starsling.dev/best-practices/github-actions/pin-action-shas
canonicalUrl: https://starsling.dev/best-practices/github-actions/pin-action-shas
---

# Pin GitHub Actions to commit SHAs

Best practice: Security. Last updated: 2026-07-06

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.

## 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`.

_Pinned to an immutable SHA_

```yaml
steps:
  # Pin to the 40-char SHA of the release you use; keep the version as a
  # comment. Run `pinact run` on your @v-pinned file to fill these in.
  - uses: actions/checkout@<40-char-sha>            # v4.2.2
  - uses: pnpm/action-setup@<40-char-sha>           # v4.1.0
```

## Avoid this

A re-pointed tag runs new code under your secrets and GITHUB_TOKEN on the next CI run.

_Mutable tags and branches_

```yaml
steps:
  - uses: actions/checkout@v4        # tag, mutable, can be re-pointed
  - uses: some-org/some-action@main  # branch, anyone with push can change it
```

## 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](https://www.wiz.io/blog/github-action-tj-actions-changed-files-supply-chain-attack-cve-2025-30066) (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.

## 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 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> # v4.2.2`) so Dependabot and Renovate still track upgrades; running `pinact run` performs this rewrite for you. 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`/`@master` rather than a 40-char hex SHA.
- Convert them in one shot with [`pinact`](https://github.com/suzuki-shunsuke/pinact) (`pinact run`), which rewrites `@v` refs to SHAs and preserves the version as a trailing comment.
- For stronger enforcement, run [`zizmor`](https://github.com/zizmorcore/zizmor) in CI: its `unpinned-uses` audit catches tag-pinned actions and `impostor-commit` catches SHAs that don't exist in the action's history.

## 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, and tools like [`pinact`](https://github.com/suzuki-shunsuke/pinact) and [`zizmor`](https://github.com/zizmorcore/zizmor) treat the whole `uses:` set uniformly. 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> # v4.2.2`). Dependabot and Renovate read that comment and still open upgrade PRs, so you get immutability and updates.

## More best practices for GitHub Actions

- [Scope id-token: write to the publishing job](https://starsling.dev/best-practices/github-actions/scope-id-token-per-job)
- [All CI best practices](https://starsling.dev/best-practices/github-actions)

## Sources

1. [GitHub - security hardening (pin actions to a full-length SHA)](https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions)
2. [pinact](https://github.com/suzuki-shunsuke/pinact)
3. [zizmor - audits](https://docs.zizmor.sh/audits/)
