# 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](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) 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](https://pydevtools.com/handbook/how-to/how-to-pin-dependencies-with-hashes-in-uv.md), 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:

```yaml
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57  # v8.0.0
- run: uv sync --locked
```

The 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](https://pydevtools.com/handbook/tutorial/setting-up-github-actions-with-uv.md) 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](https://github.com/astral-sh/setup-uv/releases/tag/v8.0.0) (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:

```bash
git ls-remote https://github.com/astral-sh/setup-uv refs/tags/v8.0.0
```

```console
$ 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](https://github.com/suzuki-shunsuke/pinact) scans `.github/workflows/` and replaces every tag pin with the corresponding SHA and trailing tag comment.

Install it with Homebrew or `go install`:

```bash
brew install suzuki-shunsuke/pinact/pinact
```
```bash
go install github.com/suzuki-shunsuke/pinact/v3/cmd/pinact@latest
```
Run it from the repository root:

```bash
pinact run
```

pinact 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`:

```yaml
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](https://github.com/zizmorcore/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](https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions) covers GitHub's official guidance on pinning and permissions.
- [The tj-actions/changed-files incident write-up](https://www.stepsecurity.io/blog/harden-runner-detection-tj-actions-changed-files-action-is-compromised) explains how the March 2025 compromise played out.
- [pinact](https://github.com/suzuki-shunsuke/pinact) automates the SHA rewrite.
- [zizmor](https://github.com/zizmorcore/zizmor) audits workflows for supply-chain and misconfiguration issues.
- [How to protect against Python supply-chain attacks with uv](https://pydevtools.com/handbook/how-to/how-to-protect-against-python-supply-chain-attacks-with-uv.md) covers the Python package side of the same threat model.
- [setup-uv v8.0.0 release notes](https://github.com/astral-sh/setup-uv/releases/tag/v8.0.0) document the move away from moving major/minor tags.
