Securely Pinning Action Versions in GitHub Actions: From Hashes and Immutable Tags to Workflow-Level Dependency Locking

Securely Pinning Action Versions in GitHub Actions: From Hashes and Immutable Tags to Workflow-Level Dependency Locking

Countermeasures against the risk of Git tag rewriting, as in the tj-actions incident
2026.05.18

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@v4 moves 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.

https://dev.classmethod.jp/articles/hardening-github-actions-with-pinact-commit-sha-semver/

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.

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".

astral-immutable-tag

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.

(astral-sh/setup-uv release notes)

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

https://github.blog/news-insights/product-news/whats-coming-to-our-github-actions-2026-security-roadmap/

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.

  1. Pin by hash value, such as actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
  2. Use an immutable tag, such as astral-sh/setup-uv@v8.1.0 (if the provider supports it)
  3. 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.

References

Share this article