GitHub Actions Workflow Lockfiles: Pin Every Action With gh actions-lock and Enforce It
GitHub Actions Workflow Lockfiles: Pin Every Action With gh actions-lock and Enforce It
A line like uses: actions/checkout@v7 does not name a piece of code. It names a tag, and the owner of that repository can point the tag at a different commit whenever they like. When we resolved it on October 15, 2026, actions/checkout's v7 tag pointed at the same commit as v7.0.1, not v7.0.0. That is normal: major-version tags are meant to move. It is also how most GitHub Actions supply-chain attacks of the last two years reached their victims. An attacker who gets write access to a popular action re-points its tags, and every workflow that trusts the tag runs the new code on its next run.
On October 14, 2026, GitHub put workflow dependency lockfiles into public preview. You keep writing @v7 in your workflows, a generated file at .github/workflows/actions.lock records the exact commit each reference resolved to, and GitHub Actions runs the recorded commit. This guide covers generating the lockfile, keeping it current, checking it in CI and turning on the Require lockfile policy without blocking your own pipelines.
What we tested and what we did not. The tag-to-commit lookups in this article were run with git ls-remote against the real actions/checkout and github/gh-actions-lock repositories on October 15, 2026. We did not run gh actions-lock itself or enable the policy on a repository for this article. The commands, flags, lockfile format and policy behaviour below come from GitHub's changelog, the three documentation pages linked at the end, and the extension's README. The CI workflow in Step 4 is our own suggested pattern built from those documented flags, not something GitHub publishes, and we have not run it.
Why SHA pinning by hand was never enough
The standard advice has been "pin every action to a full commit SHA":
1- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
It works, but it has three practical problems:
- Nobody can review it. A 40-character hash in a pull request diff tells a reviewer nothing. The trailing comment says
v7.0.1, but nothing checks that the comment is true. - It stops at the first level. A composite action can call other actions by tag. Your pinned SHA freezes the composite action's own files, but the actions it calls still move.
- It is painful to update. Teams pin once, then either stop updating or switch back to tags out of frustration.
The lockfile moves the pinning out of the workflow file. Workflows keep readable references, and the commit, including the commits of transitive dependencies from composite actions, lives in one machine-generated file that tooling can check.
What the lockfile records
The file lives at .github/workflows/actions.lock. GitHub's documentation shows this shape:
1version: 'v0.0.2'
2workflows:
3 '.github/workflows/ci.yml':
4 - 'octo-org/deploy-action@v3.1.0'
5dependencies:
6 'octo-org/deploy-action@v3.1.0':
7 ref: 'v3.1.0'
8 commit: 'sha1-...'
9 owner_id: 44036562
10 repo_id: 197814629
Two sections matter:
| Section | What it holds |
|---|---|
workflows | Which workflows are onboarded, and which dependencies each one uses |
dependencies | For each reference: the ref as written, the commit it resolved to, and the numeric owner_id and repo_id of the source repository |
The numeric IDs are worth noticing. A repository can be deleted and its name re-registered by someone else, or renamed with a redirect left behind. Recording the numeric identity means a lookalike repository under the same owner/name does not match the lock entry. The README describes this as preventing redirects and commit forgery at run time.
Onboarding is per workflow. A workflow listed under workflows is held to the lockfile; one that is not listed behaves as it always has.
Step 1: Install the extension
You need the GitHub CLI first. Then:
1gh extension install github/gh-actions-lock
2gh extension upgrade gh-actions-lock # later, to update it
When we checked the extension's repository on October 15, its newest release tag was v0.2.0. The README says the tool is pre-1.0 and that the lockfile format and behaviour may change between releases, so treat it like any other preview dependency and upgrade deliberately.
It does not support GitHub Enterprise Server. It does support Enterprise Cloud with data residency; log in to your tenant first:
1gh auth login --hostname octocorp.ghe.com
2gh actions-lock
Step 2: Generate the lockfile
From the repository root:
1# Every workflow under .github/workflows/
2gh actions-lock
3
4# Or onboard one workflow at a time
5gh actions-lock .github/workflows/ci.yml
According to the docs, this scans the workflows, resolves each reference to a commit, writes .github/workflows/actions.lock, and onboards the workflows it processed. The README says all writes are atomic: if anything fails, nothing is changed.
Expect the tool to edit your workflow files, not only create the lockfile. Two rewrites are documented:
Partial versions can become exact versions. The docs give the example octo-org/deploy-action@v3 becoming octo-org/deploy-action@v3.1.0.
Same-repository actions switch to a new $/ prefix.
1# before
2steps:
3 - uses: ./.github/actions/setup-build
4
5# after
6steps:
7 - uses: $/.github/actions/setup-build
$/ means "this repository at the commit that is running", so it is pinned by definition and needs no lock entry. The same prefix works for reusable workflows in the same repository (uses: $/.github/workflows/reusable.yml). A version suffix such as @v1 is invalid with $/. If you are not ready for that rewrite, the README documents --no-migrate-local-actions, but plain ./ local actions are skipped when a workflow is onboarded.
Commit the lockfile and the workflow edits in the same pull request, and review the lockfile diff like any other dependency change: new entries mean new third-party code will run in your CI.
1git add .github/workflows/
2git commit -m "ci: add Actions workflow lockfile"
Step 3: Keep it current
Run gh actions-lock again whenever you add a workflow with uses: lines or change a dependency.
Once a reference such as @v7 or @main is in the lockfile, GitHub does not re-resolve it on a normal run. It runs the recorded commit, even after the upstream tag moves. That is the point of the feature, but it means updates are now an explicit step:
1# Re-resolve references whose upstream tag or branch has moved
2gh actions-lock --relock
3
4# If a recorded commit is no longer reachable from its ref, --relock reports it
5# as an error instead of silently moving on. Only after you have checked why:
6gh actions-lock --relock --accept-moved
Do not reach for --accept-moved by reflex. "The commit we locked is no longer in that tag's history" is exactly what a force-pushed, hijacked tag looks like. Check the upstream release notes or security advisories first.
Dependabot keeps up for you on onboarded workflows. When it bumps an action in one, it regenerates the matching lockfile entry in the same pull request. It will not onboard a workflow that has no lockfile entries yet, so new workflows still need a manual gh actions-lock run.
Step 4: Verify the lockfile in CI
The extension has two read-only modes:
1gh actions-lock --verify # checks lockfile entries against upstream; needs network
2gh actions-lock --verify-local # offline; only checks that entries exist
Per the README, --verify checks that a commit locked under a branch or partial version is still reachable in that ref's history. --verify-local does not check whether the entries can be trusted, only that they are present.
A pull-request check that fails when someone edits a workflow without updating the lockfile could look like this. This is our own sketch, untested. It assumes the GitHub CLI preinstalled on GitHub-hosted runners and the default GITHUB_TOKEN are enough to install and run the extension:
1name: actions-lockfile
2on:
3 pull_request:
4 paths:
5 - ".github/workflows/**"
6 - ".github/actions/**"
7
8permissions:
9 contents: read
10
11jobs:
12 verify:
13 runs-on: ubuntu-latest
14 steps:
15 - uses: actions/checkout@v7
16 - name: Verify actions.lock
17 env:
18 GH_TOKEN: ${{ github.token }}
19 run: |
20 gh extension install github/gh-actions-lock
21 gh actions-lock --verify
Onboard this workflow too, so its own actions/checkout is locked.
Step 5: Enforce it with the Require lockfile policy
Enterprise, organization and repository administrators can turn on Require lockfile. GitHub documents it this way: it blocks runs of targeted workflows that use action dependencies but are not onboarded to the repository's lockfile. For onboarded workflows, GitHub Actions runs the recorded commits.
The policy is configured through GitHub's "Governing execution of GitHub Actions workflows" settings. The enforcement guide does not spell out every menu path, so follow the linked docs for your plan. The rollout GitHub recommends:
- Start in Evaluate mode, which reports what would be blocked without blocking it. Evaluate mode is only available on GitHub Enterprise.
- Scope the policy to repositories that already have a lockfile.
- Target workflows that are already onboarded.
- Review the results, then switch to Active.
On Team and Free plans there is no Evaluate mode. Onboard every workflow and confirm the Step 4 check passes before you switch the policy on, or the first push afterwards will show you what you missed by blocking runs.
Reusable workflows: onboard the shared repository first
Dependencies used inside a reusable workflow are covered by the lockfile of the repository where that reusable workflow is defined, not by the caller's. If your organization keeps shared workflows in a central repository such as org/ci-templates, the docs advise onboarding that repository first. Otherwise the callers are locked while the code they call into is not.
Best practices
- Onboard the shared workflow repository first, then the repositories that call it.
- Treat lockfile diffs as dependency changes. A new
dependenciesentry is new third-party code running with yourGITHUB_TOKENand secrets. - Keep Dependabot's
github-actionsecosystem on, so updates arrive as pull requests that already include the lockfile change. - Run
--relockon a schedule you choose, such as monthly, instead of letting locked commits age for a year. - Keep least-privilege
permissions:blocks. A lockfile controls which code runs, not what that code can reach.
Common mistakes to avoid
- Assuming the lockfile protects workflows that are not onboarded. It only applies to workflows listed under
workflows. - Turning on Require lockfile in Active mode first. Without Evaluate mode, every workflow with dependencies that is not onboarded stops running.
- Using
--accept-movedto make an error go away. Find out why the commit disappeared first. - Hand-editing
actions.lock. The file is machine-generated; regenerate it instead. - Forgetting the
./to$/rewrite when reviewing the first pull request. It is expected, not a bug. - Expecting it on GitHub Enterprise Server. GHES is not supported in this preview.
Troubleshooting
A workflow is blocked after enabling the policy. It has action dependencies but is not onboarded. Run gh actions-lock .github/workflows/<file>.yml, commit and push.
--relock reports a pin as suspicious. The recorded commit is no longer reachable from its tag or branch upstream. Compare the old and new commits and check the action's releases and advisories before using --accept-moved.
A local ./ action is skipped during onboarding. Onboarded workflows cannot use local-path actions. Switch the reference to $/..., or let the tool migrate it by running without --no-migrate-local-actions.
A Dependabot PR updated the workflow but not the lockfile. Dependabot only regenerates entries for workflows that are already onboarded. Onboard the workflow once by hand.
FAQ
Do I still need to pin actions to SHAs in the workflow file? For onboarded workflows the lockfile does that job and records transitive composite-action commits that hand-pinning misses. For workflows you have not onboarded, SHA pinning is still the protection you have.
Does the lockfile pin Docker images used by docker:// steps or container jobs?
The documentation we read describes actions, their transitive dependencies and reusable workflows. It does not say container images are covered, so pin images by digest separately.
Which plans can use it? The changelog lists Enterprise Cloud, Enterprise Cloud with data residency, Team and Free. Evaluate mode for the policy is Enterprise-only.
Is this the same as the community tools gh-actions-lockfile or ghasum? No. Those are third-party tools you run yourself, usually as a CI step. GitHub's version is applied by GitHub Actions when a workflow runs, and with the Require lockfile policy on, a team cannot opt out by removing a CI step.
What happens if the extension changes its lockfile format? It is pre-1.0 and the README warns the format may change. Pin the extension version in CI and regenerate after upgrading.
Key takeaways
| Item | Detail |
|---|---|
| Status | Public preview since October 14, 2026 |
| File | .github/workflows/actions.lock, machine-generated |
| Generate | gh extension install github/gh-actions-lock, then gh actions-lock |
| Update | gh actions-lock --relock; Dependabot updates onboarded workflows |
| CI check | gh actions-lock --verify (network) or --verify-local (offline) |
| Enforce | Require lockfile policy; Evaluate mode on Enterprise only |
| Not covered | GHES, ./ local actions, workflows that are not onboarded |
Further Reading
- GitHub Actions Now Holds Suspicious Workflow Runs Before They Touch Your Secrets
- GitHub Actions Checkout v7: Fixing the Pwn Request Vulnerability
- GitHub Actions' New vulnerability-alerts Permission: Read Dependabot Alerts Without a PAT
- GitHub Docs: Workflow dependency lockfiles, Locking action dependencies and Enforcing workflow lockfiles
- GitHub Changelog: GitHub Actions workflow lockfiles in public preview