Most workflows name their actions by tag. actions/checkout@v4 reads well, keeps working, and costs nothing to maintain. The problem is that a tag is a pointer. Whoever controls the action's repository can move it, and the next run executes the new target without a line changing in yours.
GitHub shipped workflow lockfiles in public preview on 14 October. The useful part is not the file format. It is the separation the format makes possible: the reference a human reads, and the commit the runner executes.
A tag is not a version
A tag like v4 is a name that resolves to a commit at run time. If the tag moves, the job runs a different commit, and the workflow file still says @v4.
That is not hypothetical. In March 2025, the tj-actions/changed-files action was compromised. An attacker with access to the repository moved the tags that many workflows referenced onto a malicious commit, and the action printed CI secrets into job logs in encoded form. The incident is tracked as CVE-2025-30066 and affected roughly 23,000 repositories. Every one of those workflows named a tag that had been legitimate. The tag was the vulnerability.
The standard mitigation is a full commit SHA: uses: actions/checkout@<40 hex characters>. A SHA cannot move. The costs are real, though. The workflow becomes hard to read, updates need a bot or a careful human, and the pin only covers the action you name. A composite action can call other actions by tag internally, so a top-level pin does not pin the tree underneath it.
What a lockfile records
A workflow lockfile keeps the readable reference and records what it resolved to. You write @v4; the lockfile records the commit that @v4 meant when the lock was generated. It covers the action graph, including references made from inside composite actions, which is the part per-line SHA pinning never solved.
The rules are the ones you know from package managers:
- Commit the lockfile next to the workflow.
- Regenerate it deliberately, not as a side effect of every run.
- Review its diff the way you review a dependency update.
Resolution happens once, when the lock is generated. After that, the commit is data in your repository rather than a lookup performed during the job.
What a lockfile does not cover
The format cannot express every reference. Local action paths, docker:// images, reusable workflows and expression-based refs stay outside it. A lockfile narrows the set of moving parts; it does not remove them. The remaining references should be few, and reviewed on their own.
The second gap decides whether the lockfile is worth anything: the runner has to check it. A lockfile that sits in the repository while the CI service resolves tags itself is documentation. The property you want is that a job cannot start when a reference no longer matches the recorded commit.
How preloop treats the lockfile
preloop requires .github/workflows/actions.lock and treats it as a check, not a hint. Before a job starts, every action the workflow names must resolve to the commit recorded in the lock. A missing or unreadable lockfile, a workflow the lock does not list, or an action with no pin fails the run before preloop looks up a ref. A tag that has moved fails the job instead of running the moved commit.
The same check gates credentials. A token is bound to the actions pinned in the lock, and a workflow whose actions no longer match the lock does not receive one.
References the format cannot pin are left alone rather than treated as errors. That is a deliberate boundary: the check covers what it can prove, and says nothing about what it cannot.
What changes for a team
The default becomes fail-closed. A workflow without a lock does not run until it has one. That is a migration cost, and it is also the only version of the feature that changes behavior on the day an attacker moves a tag.
Dependency review moves into code review. A lockfile diff is a short list of commits, which is a better review artifact than a workflow full of forty-character SHAs.
The exceptions stay visible. Every unpinnable reference is a known gap instead of an invisible one.
Pinning stops being a policy a team remembers and becomes a property of every job. Tags stay in the workflow because people read the workflow. Commits are what execute, because the run refuses anything else.