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:

  1. 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.
  2. 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.
  3. 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:

SectionWhat it holds
workflowsWhich workflows are onboarded, and which dependencies each one uses
dependenciesFor 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:

  1. Start in Evaluate mode, which reports what would be blocked without blocking it. Evaluate mode is only available on GitHub Enterprise.
  2. Scope the policy to repositories that already have a lockfile.
  3. Target workflows that are already onboarded.
  4. 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 dependencies entry is new third-party code running with your GITHUB_TOKEN and secrets.
  • Keep Dependabot's github-actions ecosystem on, so updates arrive as pull requests that already include the lockfile change.
  • Run --relock on 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-moved to 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

ItemDetail
StatusPublic preview since October 14, 2026
File.github/workflows/actions.lock, machine-generated
Generategh extension install github/gh-actions-lock, then gh actions-lock
Updategh actions-lock --relock; Dependabot updates onboarded workflows
CI checkgh actions-lock --verify (network) or --verify-local (offline)
EnforceRequire lockfile policy; Evaluate mode on Enterprise only
Not coveredGHES, ./ local actions, workflows that are not onboarded

Further Reading