Why this matters
When a workflow runs uses: some-org/some-action@v4, GitHub downloads whatever code the v4 tag points to at that moment. Tags and branches can be moved. If the action's repository is compromised, the attacker can point v4 at malicious code, and every workflow using it runs that code on its next trigger, with access to your secrets and your repository.
This is not a theoretical problem. In March 2025 the widely used tj-actions/changed-files action was compromised this way. Its version tags were rewritten to point at code that dumped CI secrets into build logs, and thousands of repositories were affected before the tags were fixed.
A commit SHA is different. It identifies one exact snapshot of code and cannot be changed to point anywhere else. Pinning to a SHA means the code you reviewed is the code that runs, every time.
What good looks like
Every third-party action and reusable workflow is referenced by its full 40-character commit SHA, with the human-readable version kept in a comment:
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad2be3d6a4ef3b74 # v4.2.2
- uses: shivammathur/setup-php@ccf2c627fe61b1b4d924adfcbd19d661a18133a0 # v2.32.0
Actions that live in your own repository, referenced as ./.github/actions/..., do not need pinning. They are already protected by your own branch rules.
How to do it
Pin by hand
- Open the action's repository on GitHub and go to its releases or tags.
- Click the tag you use, for example
v4.2.2, and copy the full commit SHA it points to. - Replace the tag in your workflow with the SHA and add the version as a trailing comment, as in the example above.
Pin with a tool
Doing this by hand across many workflows is tedious. Tools can rewrite every reference for you:
- pinact pins every action in a repository in one run and keeps the version comment.
- zizmor audits workflows for unpinned actions and other risky patterns, and can run in CI to stop new ones being added.
- StepSecurity Secure Repo pins actions through a web interface and opens a pull request.
Keep pins up to date
A pinned SHA does not update itself, so pair pinning with an update bot. Both Dependabot and Renovate understand SHA pins with a version comment and will open pull requests that move the SHA and the comment together.
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
Renovate handles pinned actions by default, and its helpers:pinGitHubActionDigests preset will pin any remaining tags for you.
Things to watch for
- Short SHAs. A 7-character abbreviation is not a pin. Use the full 40-character SHA.
- Reusable workflows.
uses: other-org/repo/.github/workflows/ci.yml@maincarries the same risk as an action and needs the same treatment. - Pinning without an update bot. Pins stop silent changes, but they also stop silent updates, including security fixes. The update bot brings those back as reviewable pull requests.
- Losing the version comment. Without it, nobody can tell what version a SHA is. Keep the comment; the bots rely on it too.