GitHub Actions Lockfiles Make Your CI Run the Code You Reviewed
A new gh extension pins every action to an exact commit. Here is how to adopt it in an afternoon, and the one thing a pin can never tell you.
Every uses: line in a GitHub Actions workflow is a small act of faith.
You write uses: actions/checkout@v4, someone reviews it, and the pull request merges. Six months later that workflow still says @v4. But v4 is a label, and labels can be moved. The code behind it today may not be the code anyone looked at.
GitHub's own documentation now says this out loud: "Tags and branches can move, so the code that runs is not guaranteed to be the code that the workflow author originally reviewed." On October 14 it shipped the fix into public preview: workflow lockfiles, generated by a new GitHub CLI extension called gh-actions-lock.
If you have ever used package-lock.json or poetry.lock, you already get the idea. The lockfile records exactly which commit every action resolved to, and GitHub runs that commit. A tag that moves no longer moves your CI with it. Keep one idea in your head for the rest of this piece: a lock proves sameness, not safety.
This matters more now that coding agents open pull requests too. CI is where your secrets live, and a workflow change is a short path to them.
Why a moving tag is a real problem
This is not a theoretical attack. In March 2025, attackers compromised the popular tj-actions/changed-files action. According to GitHub's advisory, they "retroactively modified multiple version tags to reference a malicious commit." The injected code scanned the runner's memory for secrets and printed them, base64-encoded, into the workflow logs. Repositories with public logs were the most exposed.
Nobody in those repositories changed a line of their own workflows. The tag changed under them.
Pinning to a full commit SHA has been the standard advice for years, and most teams skip it because it is tedious. You have to look up 40-character hashes, track the actions your actions use, and update everything by hand when a new version ships. The lockfile turns that chore into a command.
What the lockfile actually records
gh actions-lock scans every workflow under .github/workflows/ and writes one file: .github/workflows/actions.lock. GitHub's concept docs describe what each entry holds:
ref: the tag or branch your workflow names, likev4.commit: "The exact commit the reference resolved to. This is the code that will run."owner_idandrepo_id: the numeric identity of the source repository, which GitHub checks at run time.
That last pair is the clever part. Repository names can change hands; numeric IDs do not. The README says recorded identity means "redirects and mismatches are blocked at runtime," so a renamed or re-registered repo cannot stand in for the one you locked.
The lockfile also covers transitive dependencies, meaning the actions your actions call. One exception: a reusable workflow (a whole workflow you call from another repo) "is covered by the lockfile of the repository where it is defined," not yours.
Per the extension's README, GitHub also checks the lockfile itself for forgeries. A recorded SHA must actually exist in the ref it claims, and locked commits must sit within a branch's history.
Put this into practice
The extension is MIT licensed, maintained by GitHub, and its newest release is v0.2.0 from October 12. The lockfile preview is available on GitHub Enterprise Cloud (including data residency), Team and Free plans. GitHub Enterprise Server (GHES, the self-hosted edition) is not supported.
1. Install it. You need the GitHub CLI first, signed in with read access to every repository your workflows pull actions from.
gh extension install github/gh-actions-lock
2. Start with one workflow. Pick the one that touches the most secrets, usually your release or deploy workflow. GitHub's how-to guide shows the scoped form:
gh actions-lock .github/workflows/release.yml
Run it with no path to onboard every workflow at once. Either way, if the run hits an error it cannot fix, it changes nothing.
3. Read the whole diff, not just the lockfile. This surprises people. The extension also rewrites your uses: lines to the exact version it resolved, so @v3 may become @v3.1.0. References to actions inside your own repo change from uses: ./… to uses: $/…. And if your organization keeps an allowlist of approved actions with tag-pinned entries like actions/checkout@v6, the guide warns those entries will no longer match locked SHAs. Switch them to a wildcard or a full SHA before you merge.
4. Verify in CI. Add a step that checks the lockfile without writing anything:
gh actions-lock --verify
This also confirms that commits locked from a branch or a partial version are still in that ref's history. There is a faster offline option, --verify-local, but the README is clear that it "only checks that every ref has a lockfile entry," not that the entries are trustworthy. GitHub's enforcement guide uses it in a pre-commit hook as gh actions-lock --verify-local --no-fix. Keep it there, never as your CI gate.
5. Make the lockfile a reviewed file. GitHub's enforcement guide recommends adding .github/workflows/actions.lock to CODEOWNERS (the file that names who must approve changes to which paths) and requiring code owner approval. Do this. Otherwise the lockfile becomes one more file an agent can regenerate in a pull request nobody reads closely.
6. Turn on enforcement when you are ready. Admins can set a "Require lockfile" policy that "blocks runs of targeted workflows that include action dependencies but are not onboarded." There is an Evaluate mode for a dry run, but the guide notes it "is only available on GitHub Enterprise." The guide also advises against Active mode until your lockfile upkeep is reliable. Fair warning.
What to expect day to day: Dependabot keeps working. When it bumps an action in an onboarded workflow, it regenerates the matching lockfile entry in the same pull request. It will not onboard a workflow that has no lockfile entries yet; that first run is yours. When a tag moves for a legitimate reason, you run gh actions-lock --relock, look at what changed upstream, and commit.
A two-minute review for every lockfile change
Once the lockfile exists, the pull requests that touch it are the ones worth slowing down for. Here is a short routine that fits in the time it takes to read a diff.
Check which lines moved. A normal Dependabot bump changes a workflow's version string and the matching commit line together. A commit that changes while the ref stays the same means a tag moved upstream. That is exactly the event lockfiles exist to catch, so find out why before you approve.
Read the upstream change, not the hash. A 40-character SHA tells you nothing by itself. GitHub's compare view does: https://github.com/<owner>/<repo>/compare/<old-commit>...<new-commit> shows every file the action's maintainers changed between the two commits you are choosing between. For a small action that is a one-screen read. For a big one, at least skim for new network calls, new scripts and anything that touches environment variables.
Treat --accept-moved as an incident. The how-to guide reserves it for a locked commit that is no longer reachable upstream, for example because it "was force-pushed away or deleted." Legitimate projects rarely rewrite published history. When it happens, ask the maintainers before you accept.
Tell your agents the rules. If coding agents work in this repo, put a line in their instructions file (AGENTS.md, CLAUDE.md or whatever yours reads): regenerate the lockfile with gh actions-lock, never edit it by hand, and never pass --accept-moved. GitHub's own guide says never to edit the lockfile manually, so the agent and the humans follow the same rule.
None of this takes long. It just has to happen every time.
The one thing a pin cannot tell you
Here is the sentence from GitHub's own announcement that deserves more attention than it got: locking "does not show whether a dependency is safe," so review dependencies before you lock them.
A lockfile proves sameness. It says the code running today is the code that was locked on some earlier day. It says nothing about whether that code was any good on that day.
That changes where your attention goes. Before lockfiles, the risky moment was invisible: a tag moved somewhere upstream and nobody noticed. With lockfiles, the risky moment is visible and it has a name. It is the relock. It is the Dependabot pull request that changes a commit line. It is the --accept-moved flag, which you only need when a commit you locked has vanished upstream, often because someone force-pushed history away.
Treat every one of those as a code review of someone else's code, because that is what it is.
Honest limitations
It is a preview, and it says so. The README warns that "The lockfile format, command flags, and behavior may change without notice between releases." Support is best-effort through GitHub issues.
Moving pins do not move. Pins to branches or partial versions such as main or v4 "are not advanced when upstream moves." That is the point, but it means security fixes do not arrive until you relock or Dependabot does it for you. Someone has to own that chore.
Some workflows get skipped. The extension normally rewrites same-repo ./… references to $/…, but it skips a workflow when a local-path reference does not resolve to a real action file. The how-to guide says to fix the path and rerun. The README's limitations section words this more broadly and calls it a short-term gap.
Reusable workflows are someone else's lock. If you call a reusable workflow from another repo, its dependencies are covered by that repo's lockfile, not yours. If they never adopt lockfiles, neither does that part of your pipeline.
No GHES, and enforcement has plan limits. Server customers are out for now, and the dry-run enforcement mode is Enterprise only.
Actions only, not container images. The lockfile pins actions. The v0.2.0 release notes add a warning when workflows use mutable Docker image tags (names like node:20 that can point at a new image tomorrow), which tells you those are not pinned by the lockfile itself. Pin image digests separately.
Copilot helpers are not controls. The enforcement guide says the Copilot skill for lockfiles "only covers edits made with Copilot." It is a convenience. The CI check and the policy are the controls.
Where to start
You do not need a migration project. You need one workflow, one command and one CI step.
Pick your deploy workflow this week. Run gh actions-lock on it, read the full diff, add --verify to CI and put the lockfile under CODEOWNERS. Then watch what happens the next time Dependabot or an agent proposes a bump: you will see a commit hash change in a file someone is required to review.
That visible moment is the whole value. What you do with it is still up to you.
Sources: GitHub changelog: workflow lockfiles in public preview (October 14, 2026), GitHub Docs: workflow dependency lockfiles, lock workflow dependencies, enforce workflow lockfiles, github/gh-actions-lock README (v0.2.0, October 12, 2026, reviewed), GitHub advisory GHSA-mrrh-fwg8-r2c3 (tj-actions/changed-files).
Medium metadata
Title: GitHub Actions Lockfiles Make Your CI Run the Code You Reviewed
Alternate titles to test:
- Your GitHub Actions Tags Can Move. Lockfiles Finally Stop That.
- You Don't Have to Hand-Pin GitHub Actions SHAs Anymore
Subtitle options:
- A new gh extension pins every action to an exact commit. How to adopt it, and what a pin can never tell you. (picked)
- One command, one CI step, one CODEOWNERS line. Here is the afternoon version.
- The tj-actions attack worked because tags move. GitHub just made that harder.
Subtitle (final, under 130 characters): A new gh extension pins every action to an exact commit. How to adopt it, and what a pin can never tell you.
Description: GitHub's new gh-actions-lock pins every Actions dependency to a commit. How it works, how to adopt it in an afternoon, and its honest limits.
Tags (priority order):
- GitHub Actions: the exact feature and audience.
- Software Supply Chain: the threat the feature addresses.
- DevSecOps: CI security practice.
- AI Agents: agent-authored pull requests raise the stakes.
- Programming: broad developer reach.
Article type: A (spike). A real release with a one-command payoff.
Publish plan: Post at 7 to 8 AM ET. Let Medium distribute for 2 to 6 hours, then send to subscribers only if it is performing.