Decision Always pin GitHub Actions to a commit hash

accepted

GitHub Actions referenced by a mutable tag or branch can be silently replaced with malicious code, exposing CI/CD secrets across every repository that uses them.

Decision

All GitHub Actions must be pinned to a full commit SHA rather than a tag or branch name.

# Bad - mutable, can be silently replaced
- uses: actions/checkout@v4

# Good - immutable, tied to a specific commit
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0

Include the corresponding version tag as a comment so the pinned commit remains human-readable and can be updated intentionally.

Enforcement

At the organization level

GitHub now supports enforcing SHA pinning at the organization level. Enable this policy for GitHub organizations when possible. This causes any workflow using a non-pinned action to fail outright.

At the project level

Projects should pin all actions regardless of whether the organization policy is in place. Use Renovate to keep pinned hashes up to date automatically by adding the helpers:pinGitHubActionDigests preset to the extends array:

{
  "extends": ["helpers:pinGitHubActionDigests"]
}

Renovate will update the commit SHA whenever the referenced tag moves, keeping the pin current without manual effort.

Background

In March 2025, a cascading supply chain attack compromised the widely used tj-actions/changed-files action by pushing malicious code to an existing tag. Any repository referencing the action by tag automatically ran the malicious code and exposed CI/CD secrets. Pinning to a commit SHA prevents this class of attack entirely: a tag can be moved to point at new code, but a SHA is immutable.

Additional resources

While not mandatory, projects can use the Zizmor tool to perform static checks on GitHub Actions, ensuring that all actions are pinned to a specific commit SHA.

Consequences

All existing workflows should be audited and updated to use commit SHA pins. New workflows must follow this pattern from the start.

Pinned SHAs require active maintenance to stay current. Using Renovate with the helpers:pinGitHubActionDigests preset automates this, making the maintenance overhead negligible.