GitHub Actions in December: workflow lockfiles and the end of open pull_request_target
Workflow lockfiles are GA and pull_request_target gets blocked in public repos on Dec 2. How to audit and lock down your CI before the deadline.
Igor Calabraro
· 7 min read
In this post
Almost every Next.js or Node project I come across runs tests, lint and deploys through GitHub Actions. And the pipeline is almost always treated as an afterthought: YAML copied from another repo, actions referenced by tag and, sometimes, a pull_request_target nobody remembers adding. Within a single week, GitHub changed both of those things. On October 28, workflow lockfiles became available on every plan. On November 2, the company announced that pull_request_target will be blocked by default in public repositories starting December 2. My take: CI is production code with access to your secrets, and now there is both a deadline and a tool to treat it that way.
Workflow lockfiles GA (Free, Team, Enterprise Cloud)
2026-11-09, 16:00–18:00 UTC
First brownout: pull_request_target temporarily blocked
2026-11-16, 16:00–18:00 UTC
Second brownout
2026-12-02
Permanent block by default in public repositories
During those windows, affected workflows don't execute and show up as failed runs with an error annotation. Outside them, the rule stays in evaluate mode: workflows run, but with a warning.
Private and internal repositories are not affected, and neither is GitHub Enterprise Server. If your repo is public (an open source project, a template, a portfolio), this is on you.
The difference between the triggers is context. pull_request runs the PR's code with reduced permissions, and a PR from a fork doesn't get the repository's secrets. pull_request_target, on the other hand, runs in the context of the base repository, with access to secrets, as GitHub's own announcement puts it. It was designed for tasks that don't need to execute the PR's code, such as labeling or commenting.
The trouble starts when someone combines that trigger with a checkout of the fork's code:
# DON'T do this: untrusted code running with secretson: pull_request_targetjobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - run: npm install && npm test
A malicious postinstall in the fork's package.json, or a tampered test, now runs with a write token and your deploy secrets. This pattern became known as a "pwn request", and it's exactly what the block is meant to stop.
Before the first brownout, it's also worth opening Actions Insights, which GitHub points to for seeing which runs would be blocked. For each workflow you find, there are three ways out: drop the trigger, move to the pattern below or, if it's truly needed, allow it explicitly in an Actions event policy, which can target specific workflow files.
#Step 2: split the unprivileged build from the trusted step
The pattern I recommend for most cases is two workflows. The first runs on pull_request, with no secrets, and only produces an artifact:
The second fires when the first one finishes. It runs the base repository's code, so it can have write permissions, but it treats the artifact as untrusted input:
Two details matter here. tr -cd '0-9' guarantees that only a number reaches the command, even if the fork tampered with the file. And event values go through environment variables instead of being interpolated straight into run, which prevents shell injection.
The second announcement targets a different vector: the tag that changes hands. When you write uses: some-org/some-action@v3, you're trusting that v3 will keep pointing at safe code. If the maintainer's account is compromised, the tag can be moved to another commit, and your pipeline runs the new version without any PR.
The lockfile fixes this by keeping the readable reference in the YAML while running the commit you locked. It's generated by GitHub's official CLI extension:
gh extension install github/gh-actions-lock# from the repo root: every workflowgh actions-lock# or one workflow at a timegh actions-lock .github/workflows/pr-build.yml
The extension writes .github/workflows/actions.lock, covering actions and their transitive dependencies, and rewrites uses: to the exact version it resolved. For example, @v3 may become @v3.1.0, pointing at the same commit. Local ./… references become $/…, which always resolve to the running commit.
For day-to-day work, three points:
Dependabot already understands the lockfile: when it opens a version bump for an action in a locked workflow, it updates the lockfile entry in the same PR. It doesn't onboard new workflows.
A tag that moved:gh actions-lock --relock re-resolves it. If the old commit is gone (force-push or deletion), you need --relock --accept-moved, and that deserves investigation before merging.
Review the lockfile diff like any dependency bump: a new commit is new code in your CI. A different owner_id or repo_id means the repository behind that name is no longer the same one.
Admins can also require lockfiles with the "Require lockfile" policy at the enterprise, organization or repository level, and check in CI that a lockfile is up to date without modifying any files.
This isn't an isolated move. On November 2, Vercel started blocking malicious npm package versions before and during installation, on every plan and with no configuration. If a project's lockfile points at a flagged version, the deploy fails and names the package. The direction is the same: the dependency chain, from package.json to the CI YAML, is now attack surface that gets the same scrutiny as application code.