Pin GitHub Actions to a commit SHA so a moved tag cannot change your build
To pin GitHub Actions to a commit SHA, replace the tag after the @ with the full 40-character commit SHA it points to, and keep the version in a trailing comment: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1. A tag can be moved to different code at any time, as the tj-actions/changed-files compromise showed in March 2025; a full SHA always names the same commit.
Every uses: line in a workflow is a dependency that resolves when the job starts. When the reference is a tag, the code that runs is whatever the tag points at in that moment, which need not be what you reviewed when you added it. That is the gap the tj-actions/changed-files attack used: nobody edited the victims' workflows, the attacker changed what the tags meant.
The March 2025 incident is the worked case. Around it: why a tag moves, how to pin and keep pins current, and the controls GitHub has shipped to make pinning a rule rather than a habit, including workflow lockfiles, generally available since October 28, 2026.

Why a tag is not a version
A git tag is a name for a commit. A lightweight tag is nothing more than a ref holding a SHA, and anyone who can push to the action's repository can delete it and create it again pointing somewhere else. GitHub's secure use reference warns that there is risk in pinning to a tag "even if you trust the author, because a tag can be moved or deleted if a bad actor gains access to the repository storing the action."
Maintainers move tags on purpose. actions/checkout@v7 is meant to follow the newest v7 release, so the commit it names changes with every patch. On 2026-11-12, v7 and v7.0.1 named the same commit and v7.0.0 named another: a workflow that said @v7 before the 7.0.1 release ran different code after it, with no change to the file. That is useful when the maintainer moves the tag and dangerous when somebody else does.
git ls-remote https://github.com/actions/checkout 'refs/tags/v7*'
3d3c42e5aac5ba805825da76410c181273ba90b1 refs/tags/v7
9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 refs/tags/v7.0.0
3d3c42e5aac5ba805825da76410c181273ba90b1 refs/tags/v7.0.1The worked case: tj-actions/changed-files, March 2025
tj-actions/changed-files lists the files a change touched, and the GitHub advisory describes the attack as impacting over 23,000 repositories. On March 14, 2025 an attacker added a malicious commit, 0e58ed8671d6b60d0890c21b07f8835ace038e67, and retroactively repointed existing version tags to it. The advisory names v1.0.0, v35.7.7-sec and v44.5.1; StepSecurity, which detected it, found that most release tags had been moved.
The payload targeted the Runner Worker process, scanned its memory for secrets and printed them to the build log, base64 encoded twice. On a public repository the log is public, so every secret the job could see became readable by anyone who opened the run. It is CVE-2025-30066, patched in v46.0.1, and CISA connected it to an earlier compromise of reviewdog/action-setup@v1.
A workflow pinned to an earlier full SHA kept running that commit, because moving a tag does not change what a SHA names. Anyone who had pinned to 0e58ed86 itself had to update: a pin protects the code you reviewed, not code you never looked at.
| When (UTC) | What happened | Source |
|---|---|---|
| March 11, 18:42 to 20:31 | reviewdog/action-setup@v1 compromised | CISA |
| March 14, about 16:00 | Incident starts | StepSecurity |
| March 14, 17:00 | Most versions of the action confirmed compromised | StepSecurity |
| March 14, 20:00 | Public repositories found leaking secrets in build logs | StepSecurity |
| March 15, 14:00 | GitHub removes the action, so workflows cannot use it | StepSecurity |
| March 15, 22:00 | GitHub restores the repository with every version cleaned | StepSecurity |
How to pin: the full SHA and a version comment
Resolve the tag to its commit from the action's own repository, not a fork (the secure use reference asks you to check exactly that), and paste all 40 characters in place of the tag. GitHub's enforcement policy checks for a full commit SHA, so a short one fails it. Keep the version as a comment at the end of the line: reviewers read it, and Dependabot and Renovate use it to know which release to move to.
Pin every action you do not own, GitHub's included. The setting that enforces pinning covers actions authored by GitHub and by your own organization, so actions/checkout@v7 fails under it like any other tag. Reusable workflows can still be called by tag.
# Before: three tags, resolved again at the start of every job
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- uses: tj-actions/changed-files@v47
# After: three commits, each version kept as a comment
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
- uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6Keeping pins current with Dependabot or Renovate
A pin that never moves stops receiving fixes, so the other half of pinning is a bot that proposes new pins as pull requests. Dependabot reads .github/workflows when its github-actions entry uses the directory /. The secure use reference notes one behavior worth knowing: if the pinned commit is not associated with any tag, Dependabot updates it to the latest commit, which might not be the latest release. Pin to tagged release commits and that does not come up.
Renovate updates the SHA according to the tag in the comment, and its helpers:pinGitHubActionDigests preset converts tag references into pinned SHAs for you. Its documentation notes that an action pinned to a bare SHA with no version comment is not updated by default, because Renovate cannot tell which tag the SHA belongs to. Either way, read the diff: the bot is changing which code runs.
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Make it a rule: the controls GitHub shipped
Pinning by convention lasts until the next pull request that adds @main. GitHub has shipped three controls that turn it into policy, each set under Actions in the enterprise, organization or repository settings.
| Control | Shipped | What it does |
|---|---|---|
| Require actions to be pinned to a full-length commit SHA | August 15, 2025 | Any workflow that uses an action by tag or branch fails. Reusable workflows can still be referenced by tag. |
Blocking with ! in the allowed actions policy | August 15, 2025 | Blocks a specific action or version. The block list is evaluated last, so it overrides any rule that would allow the action: the fast response to an advisory. |
| Workflow lockfiles and the Require lockfile policy | Public preview October 14, 2026; generally available October 28, 2026 | Records the commit behind every action reference, transitive ones included, in .github/workflows/actions.lock. Require lockfile enforces adoption. |
Lockfiles: the pin without the SHA in your YAML
A lockfile moves the pin out of the workflow. You keep readable references such as @v7.0.1, and gh actions-lock, a GitHub CLI extension, writes .github/workflows/actions.lock with the commit each reference resolved to and the numeric owner and repository IDs it came from. Once a workflow is onboarded, every action it uses must have an entry, and the job runs the recorded commit even if the tag has since moved. GitHub's docs say that for a repository that adopts a lockfile, SHA pinning in workflow files is no longer necessary.
It also reaches further than inline pins, because it covers transitive dependencies: the actions your actions call. When Dependabot bumps an action in an onboarded workflow it regenerates the matching entry in the same pull request. The extension's README lists what it does not cover: reusable workflows called from another repository, local ./ actions, and container images, which only draw a warning when they lack an @sha256: digest.
gh extension install github/gh-actions-lock
gh actions-lock # onboard or fix: pin every resolvable action, write actions.lock
git add .github/workflows/
gh actions-lock --verify # in CI: the same checks, without writing anythingWhat a pin does not do
A pin fixes which code runs. It does not make that code safe, and GitHub says the same of lockfiles: they do not determine whether a dependency is safe. Read an action before you pin it.
A pin also stops at its own line. An inline SHA does not pin what a composite action calls inside itself, or a container image the action pulls by tag; the lockfile reaches the first, and an image digest is the answer to the second. And a pinned action still runs with everything the job holds, so pair this control with scoping the GITHUB_TOKEN: the less a job holds, the less a compromised step can print into a log.
Key takeaways
- A tag resolves when the job starts, so the code that runs can change without any edit to your workflow.
- Pin to the full 40-character SHA from the action's own repository, with the version in a trailing comment.
- Dependabot and Renovate keep pins current; review their pull requests as code changes.
- Enforce it: the SHA pinning policy (August 2025) or a workflow lockfile with Require lockfile (GA October 2026).
Frequently asked questions
Does Dependabot update GitHub Actions pinned to a commit SHA?
github-actions entry in .github/dependabot.yml, Dependabot proposes new pins as pull requests. If the pinned commit is not associated with a tag, GitHub notes that Dependabot moves it to the latest commit rather than the latest release, so pin to tagged release commits.How do I find the commit SHA for a GitHub Action version?
git ls-remote https://github.com/<owner>/<repo> refs/tags/<tag> against the action's own repository. If a second line ends in ^{}, the tag is annotated and that line holds the commit. Check the repository is the original and not a fork before you paste the SHA.Should I pin GitHub's own actions like actions/checkout to a SHA?
Was my repository affected by the tj-actions/changed-files compromise?
Is a workflow lockfile better than pinning to a SHA?
Related guides
References
- GitHub Advisory GHSA-mrrh-fwg8-r2c3, tj-actions/changed-files (CVE-2025-30066), read 2026-11-12
- StepSecurity: tj-actions/changed-files action is compromised (read 2026-11-12)
- CISA: supply chain compromise of tj-actions/changed-files and reviewdog/action-setup (read 2026-11-12)
- GitHub changelog: Actions policy now supports blocking and SHA pinning actions (2025-08-15)
- Workflow dependency lockfiles, docs.github.com (read 2026-11-12)
- GitHub Actions documentation