Pinning GitHub Actions to a SHA is not protection
In March 2025 an attacker compromised tj-actions/changed-files and rewrote its release tags to point at malicious code. The injected code dumped runner memory into workflow logs, which on a public repository means printing your secrets where anyone can read them. Around 23,000 repositories depended on that action, most of them through a mutable tag like @v45.
Repositories that had pinned the action to a commit SHA were unaffected. That is worth sitting with for a moment, because it is rare for a security practice to draw such a clean line through a real incident. The advice worked exactly as advertised.
So we all pinned. And then we quietly undid it.
What pinning actually buys you
uses: actions/checkout@v5 means "run whatever v5 points at when this workflow starts". The tag is a pointer the upstream author can move at any time, and nothing about your repository changes when they do. You are trusting the tag to keep meaning what it meant when you wrote the line.
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 means "run this exact commit". Nobody can repoint it. If the upstream repository is compromised tomorrow, your workflow keeps running the code you reviewed.
That is the entire mechanism. It is not clever, and that is its strength.
The part nobody mentions
A pin is a snapshot, and snapshots go stale. The action you froze in March has since fixed bugs, gained features, and patched its own vulnerabilities, and you are running none of it. So you enable Dependabot, which is exactly the right call — an un-updated pin is its own risk.
Now look at what arrives in your pull request queue:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
+ uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
What changed? You cannot tell. Not because you are careless — because the diff contains no information. Two hex strings differ. That is the whole change.
GitHub's own engineering blog describes a repository where roughly one in six commits were Dependabot version bumps, 61 of them in twelve months, sometimes several in a single day. Nobody meaningfully reviews an opaque hash 61 times a year. You approve it, because the alternative is manually diffing a third-party repository before every merge, and nobody has budgeted for that.
One maintainer put it plainly on her own blog, admitting she blindly accepts Dependabot pull requests and noting this negates the benefit of pinning. That is not a confession of bad practice. It is an accurate description of what the workflow makes people do.
So the pin is still in the file. The review it depends on is gone. You are running unreviewed third-party code with your credentials, with a line in your YAML that looks like diligence.
It gets slightly worse
Two details make this sharper than it first appears.
Dependabot Alerts do not cover SHA-pinned actions. Users have been asking GitHub to fix this asymmetry for a while. You are told to pin, then told to enable version updates to combat staleness, and the alerting that would tell you a pinned action has a known vulnerability does not apply to the form you were told to use.
The version comment is not verified by anything. Tooling writes bumps like this:
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
That line makes two claims. First, run this commit — GitHub enforces it. Second, this commit is v5.1.0 — nothing enforces it at all. It is a comment. Your eyes read v5.1.0 and move on, and the only thing GitHub cares about is the hash.
Those two claims agreeing is precisely what a retagged release breaks. In the tj-actions incident the tags were moved; the SHAs were not. If a pin's comment says v5.1.0 and the v5.1.0 tag now resolves to a different commit, something worth knowing has happened — either your pin is stale, or the tag moved under you. That check takes one API call, and almost nobody performs it.
This is not a niche concern
GitHub reported 11.5 billion Actions minutes used in 2025, up 35% year over year. Datadog's security research found that two out of three organizations have at least one vulnerability in an Actions workflow.
And the incidents kept coming after tj-actions. In May 2026 a threat group compromised 5,561 public repositories in about six hours by pushing workflows carrying dormant workflow_dispatch backdoors. TanStack's router was breached through cache poisoning that led to OIDC token theft and 84 malicious npm packages.
Attackers moved to CI because CI holds credentials, runs on every push, and is reviewed less carefully than application code. Workflow files are code with production access that we treat as configuration.
Sources above were paraphrased; content was rephrased for compliance with licensing restrictions.
What would actually help
Not "review your Dependabot PRs more carefully." That advice has been available the whole time and it loses to arithmetic: 61 bumps a year against a finite attention budget.
What helps is making the diff contain information again. When a pin moves, the questions worth answering are mechanical, and mechanical questions can be answered mechanically:
Does the new commit exist in any tag or branch upstream, or is it floating outside published history?
If the pin claims a version, does that tag still resolve to this commit?
Did the action's declared interface change — inputs, outputs, required flags?
Did its execution model change, for instance a JavaScript action becoming a Docker action that can pull an arbitrary image?
Did it gain network calls, secret references, or dynamic code execution that it did not have before?
None of that requires judgment. It requires fetching two revisions and comparing them, which is a thing a computer should do for you before you click approve.
Two honest caveats
No static check can be certain. An action that builds a resource name at runtime could reach anything, and a tool claiming certainty there is lying. The right behaviour is to say what could not be determined rather than stay quiet.
A clean report is not a safety guarantee. It means the reviewable surface did not change in a way worth flagging. A sufficiently careful attacker can stay below that line. This raises the cost of an attack; it does not eliminate it.
The point
Pinning to a SHA is still correct. Keep doing it.
But recognise what it is: a mechanism that converts a trust problem into a review problem. It only pays out if the review happens, and we built a workflow that guarantees it does not. The pin in your YAML is not protection. It is a place where protection could go, if something were reading it.
I wrote action-diff to do the comparison above — it reads both revisions of a bumped action and reports what changed, including whether the pin's claimed version still resolves to the commit it names. It is MIT licensed and runs as a GitHub Action or a CLI.
Use it, use something else, or write your own. The tooling matters much less than noticing that a practice we all adopted has quietly stopped working.
