Securely Pinning Action Versions in GitHub Actions: From Hashes and Immutable Tags to Workflow-Level Dependency Locking
This page has been translated by machine translation. View original
The March 2025 compromise incident of tj-actions/changed-files (CVE-2025-30066) involved rewriting version tags, and it once again highlighted the importance of specifying actions securely.
This article explains the following two measures that are already available
- Commit hash
- immutable tag
and the following feature, which is scheduled for release within 2026
- Workflow-Level Dependency Locking
Problems with the Naive Way of Specifying Actions
Specifications such as uses: owner/action@v4, which appear in GitHub Actions samples, are convenient, but they have structural problems on both the provider side and the consumer side.
Provider perspective
- Git tags can be rewritten. They can be redirected to a different commit
- The major version of a tag is dynamic. A major tag such as
actions/checkout@v4moves every time a new minor/patch is released
Consumer perspective
- When the above happens, the referenced target changes even though the version specified in the workflow file has not changed.
In short, a specification such as uses: owner/action@v4 is convenient, but it is not immutable.
There are 2 officially provided ways to create references that cannot be swapped out.
Pin by Commit Hash
This is the method of writing @<Git commit SHA> directly. A Git commit SHA is guaranteed to be immutable.
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Since a hash value alone doesn't tell you which version it is, this practice goes hand in hand with adding the version number in a comment.
Using a tool like pinact, you can easily achieve hash values with version comments.
Tools that update dependencies, such as Dependabot and Renovate, also support updating hash values.
There are also options to enforce pinning actions to commit SHAs at the Organization or repository level.
- Verifying GitHub Actions' Enforce SHA Pinning
- GitHub Actions: Caveats of Enforce SHA Pinning and the Current Best Solution
Note that for Impostor Commits (commits stacked on a fork), you need to use other tools such as zizmor in combination.
Use immutable tags
This is the method of using tags that support immutable releases, which GitHub made generally available (GA) on October 28, 2025.
- uses: astral-sh/setup-uv@v8.1.0
At first glance at the configuration, it looks the same as before.
The astral-sh/setup-uv release list shows the label "Immutable".

When you use immutable releases, a version that has been issued once cannot be reused. If there is a problem with a release, even for the most trivial commit, it is re-released as a new version.
If major/minor versions such as v8 or v8.0 are provided in the v<Major>.<Minor>.<Patch> format, there is a problem that versions and resources no longer map 1:1.
Therefore, astral-sh/setup-uv and others go a step further and have adopted a policy of not releasing major/minor versions from v8 onward.
To increase security even more we will stop publishing minor tags. You won't be able to use @v8 or @v8.0 any longer. We do this because pinning to major releases opens up users to supply chain attacks like what happened to tj-actions.
- uses: astral-sh/setup-uv@v7
When updating the above to
- uses: astral-sh/setup-uv@v8
instead of that, use
- uses: astral-sh/setup-uv@v8.1.0
You must specify down to the patch version like this, otherwise it will come up empty.
To enable immutable releases, check "Enable release immutability" under the repository's Settings → General → Releases.
The Next Generation: Workflow-Level Dependency Locking
GitHub announced its security roadmap in late March 2026.
With Workflow-Level Dependency Locking, scheduled for release by September 2026, using a model like Go's go.mod (versions) + go.sum (checksums), you declare the version in the uses: section and the version + hash value in a new dependencies: section (meaning mismatches between a version and its hash value can also be detected). Just as go.sum also covers transitive dependencies, this feature is planned to cover nested dependencies such as composite actions.
name: build-and-attest
on:
workflow_dispatch:
permissions:
contents: read
id-token: write
attestations: write
jobs:
build-and-attest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/attest-build-provenance@v3
with:
subject-path: README.md
dependencies:
- github.com/actions/checkout@v6.0.1:sha1-8e8c483db84b4bee98b60c0593521ed34d9990e8
- github.com/actions/attest-build-provenance@v3.1.0:sha1-00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8
- github.com/actions/attest@v3.1.0:sha1-7667f588f2f73a90cea6c7ac70e78266c4f76616
Conclusion
The ways to securely specify GitHub Actions actions are as follows.
- Pin by hash value, such as
actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - Use an immutable tag, such as
astral-sh/setup-uv@v8.1.0(if the provider supports it) - Within 2026, a more robust mechanism like Go's go.mod + go.sum (Workflow-Level Dependency Locking) is planned to be provided
Because immutable releases (tags) also depend on the action provider, I recommend prioritizing consistency and starting with hash value pinning.