How to pin GitHub Actions by SHA for Python projects
Tag-based pins like @v7 are mutable references that the action’s maintainer (or anyone who compromises their account) can repoint at malicious code, which then runs with access to your CI secrets. The March 2025 tj-actions/changed-files compromise leaked secrets from thousands of repositories precisely this way. Pinning every third-party action to a full 40-character commit SHA makes the pin immutable and cuts off that attack path. This is the same principle as pinning Python dependencies with hashes, applied one layer up to your CI.
Pinning an action to a SHA
Replace the tag in every uses: line with the full commit SHA, and keep the human-readable version as a trailing comment:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- run: uv sync --lockedThe SHA is what GitHub Actions resolves, so the comment has no runtime effect. It tells humans, Dependabot, and scanners which release the SHA corresponds to. See the setup-uv tutorial for the broader uv-in-CI workflow this slots into.
Note
astral-sh/setup-uv stopped publishing moving v8 and v8.0 tags in its v8.0.0 release (March 2026). Only immutable full-version tags like v8.0.0 are published, which nudges consumers toward SHA pinning or explicit full-version tags.
Finding the SHA for a tag
Use git ls-remote against the action’s repository to resolve a tag to its commit SHA without cloning:
git ls-remote https://github.com/astral-sh/setup-uv refs/tags/v8.0.0$ git ls-remote https://github.com/astral-sh/setup-uv refs/tags/v8.0.0
cec208311dfd045dd5311c1add060b2062131d57 refs/tags/v8.0.0
The 40-character hash is the SHA to paste into uses:. The GitHub UI also exposes it: open the repository’s tags page, click the tag, and copy the commit hash from the URL or the commit header.
Converting a whole workflow with pinact
Rewriting dozens of uses: lines by hand invites mistakes. pinact scans .github/workflows/ and replaces every tag pin with the corresponding SHA and trailing tag comment.
Install it with Homebrew or go install:
brew install suzuki-shunsuke/pinact/pinactRun it from the repository root:
pinact runpinact edits every workflow file in place. Review the diff, commit, and the repository is fully SHA-pinned. Run pinact run --check in CI to fail builds that introduce an unpinned uses: line.
Keeping pinned actions updated with Dependabot
A SHA pin that nobody updates rots into an unpatched dependency. Dependabot parses the # v8.0.0 trailing comment, checks upstream for newer releases, and opens pull requests that bump both the SHA and the comment together.
Create .github/dependabot.yml:
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Dependabot then opens one PR per action whenever a new release ships. Each PR’s diff shows the SHA change alongside the release notes, so reviewing an action update takes seconds.
Verifying the SHA matches the tag
Pinact trusts whatever SHA GitHub currently returns for a tag, which is fine when the tag has not been force-moved. For a stronger check, run zizmor against the workflows directory. zizmor audits GitHub Actions workflows for common security issues, including unpinned actions and suspicious patterns, and is a useful companion to pinact in CI.
When a moving ref is fine
Internal actions inside the same organization have the same trust boundary as the calling repository: a compromise of my-org/internal-action is already a compromise of my-org. Pinning my-org/internal-action@main is reasonable for these, as is pinning actions published from the same repository as the workflow. Reserve SHA pinning for every third-party action outside that trust boundary.
Learn More
- Security hardening for GitHub Actions covers GitHub’s official guidance on pinning and permissions.
- The tj-actions/changed-files incident write-up explains how the March 2025 compromise played out.
- pinact automates the SHA rewrite.
- zizmor audits workflows for supply-chain and misconfiguration issues.
- How to protect against Python supply-chain attacks with uv covers the Python package side of the same threat model.
- setup-uv v8.0.0 release notes document the move away from moving major/minor tags.