What the Malicious Commit Did
The payload was a Node.js function carrying base64-encoded instructions. At runtime it decoded and executed Python code that scanned the runner's memory for credentials. Not subtle, but it didn't need to be subtle. The runner had already loaded everything worth stealing.
What went into the public build logs: GitHub PATs. npm tokens. Private RSA keys. AWS access keys. Anything the workflow had loaded into environment variables, anything passed as a secret via ${{ secrets.* }}, anything a prior step had exported. CI runners are credential intersections, they hold deploy keys, registry tokens, cloud credentials, signing keys, all at once. That's the entire value proposition of CI: one place that touches everything. It's also the attack surface.
The Upstream Chain
The compromise didn't start with tj-actions. It traced back through reviewdog/action-setup@v1 (CVE-2025-30154). The chain was: reviewdog compromised → tj-actions downstream → 23,000 repositories downstream of that. Supply chain attacks compound. Each layer of trust amplification means the blast radius multiplies. You audit your own code. You probably don't audit every action you pull in. You definitely don't audit every action that your actions pull in.
The Fix That's Been Available Since Day One
SHA pinning.
# This is a lie:
uses: tj-actions/changed-files@v45
# This is a pin:
uses: tj-actions/changed-files@a538f65f2c3e28a5d4c9a2e5d9e3e4f1a2b3c4d5
A SHA is immutable. You cannot retroactively move a commit hash. If you pin to a full SHA, the attacker would need to find a preimage collision against SHA-1 to serve you different code, and GitHub's object store uses SHA-1 with collision detection on top. The tag approach offers zero cryptographic guarantees. The SHA approach offers near-absolute ones.
This has been true since GitHub Actions launched. The documentation mentions it. Security tooling like step-security/harden-runner and pin-github-action automate it. None of that matters if nobody does it.
Nobody does it because it's ugly. Tag-pinned workflows are readable, @v45 tells you something. SHA-pinned workflows are opaque, forty hex characters tell you nothing at a glance. Updating a SHA pin requires a deliberate lookup rather than bumping a version number. It's friction. Small friction, but friction.
That friction just cost 23,000 repositories their CI secrets.
The Structural Problem
This isn't a bug in tj-actions. The maintainer didn't write malware. This is how tags work, and the GitHub Actions ecosystem was built on the assumption that action authors are trustworthy and accounts don't get compromised. Both assumptions fail routinely.
Every third-party action in your workflow is an unconditional code execution grant. uses: is curl | bash, but with version theater instead of versioning. The tag gives you the illusion of control. The SHA gives you the reality.
The mental model most teams operate with, "I'm using a pinned version, so I'm reproducible", was always wrong. Version tags communicate intent, not immutability. Semver is a convention upheld by social contract. The contract breaks when an account is compromised, when a maintainer goes malicious, when someone sells a popular package to a bad actor. It breaks quietly, with no indication in your workflow file.
What to Do
Pin every action to a full commit SHA. Not a tag, not a branch, a SHA. Add a comment with the human-readable version for when you need to update:
# tj-actions/changed-files@v46.0.1
uses: tj-actions/changed-files@4edd678ac3f81e2dc578756871e4d00c19191daf
Use tooling to automate the maintenance: step-security/harden-runner, Dependabot's actions ecosystem updates, or pin-github-action as a pre-commit hook. These all exist. The problem isn't tooling availability, it's that the default is insecure and the secure option requires work.
The GitHub Actions marketplace has no code review, no security audit, and no cryptographic signing requirement. It's a package registry where the install command is trust. The tj-actions incident won't be the last one. It wasn't even the first.
Pin to SHAs. The inconvenience is real. So was the credential dump.