← All posts
·8 min read·GuardLayer

Refusing to check out fork pull request code: the fix

GitHub ActionsCISecretsSupply Chain

Refusing to check out fork pull request code from a 'pull_request_target' workflow means actions/checkout caught the pwn-request pattern: the workflow holds your repository's secrets and was about to check out a fork's code. Don't silence it with allow-unsafe-pr-checkout: true. Move the build to a pull_request workflow, where fork PRs get no secrets, and keep pull_request_target for jobs that never run PR code.

If this started failing in a workflow nobody touched, that's expected: GitHub made actions/checkout v7 generally available on June 18, 2026 with this check on by default, then backported it to the floating major tags on July 20, 2026. A workflow on actions/checkout@v4 picked it up without a single commit. @v1 did not, and neither did steps pinned to a SHA or to a minor or patch version.

The full message, as thrown by the action's source (identical in v4.4.0, v6.1.0 and v7.0.1):

Refusing to check out fork pull request code from a 'pull_request_target' workflow. This workflow runs with the base repository's GITHUB_TOKEN, secrets, default-branch cache scope, and runner access. Fetching and executing a fork's code in that trusted context commonly leads to "pwn request" vulnerabilities. To opt in, review the risks at https://gh.io/securely-using-pull_request_target and set 'allow-unsafe-pr-checkout: true' on the actions/checkout step.

In a workflow_run workflow, the quoted event name reads 'workflow_run' instead.

Why does actions/checkout refuse to check out fork pull request code?

Because your step matches the exact shape of a pwn request. The guard, in src/unsafe-pr-checkout-helper.ts, throws only when all three of these hold:

  1. The event is pull_request_target, or workflow_run started by a pull_request* event.
  2. The pull request comes from a fork: its head repository ID differs from the base repository's.
  3. The step points at fork code: repository: is the fork, ref: is refs/pull/<n>/head or refs/pull/<n>/merge, or ref: is a PR commit SHA from the event payload (the head SHA, or under pull_request_target also the merge commit SHA).

PRs from branches in your own repository pass, and the plain pull_request event is untouched. So the error only appears on PRs from forks, which is where untrusted code comes from.

Why is checking out the PR head dangerous?

pull_request_target runs the workflow file from your default branch and gives the job your repository and organization secrets plus the base repository's GITHUB_TOKEN. That's safe while it only runs your code. Once a step checks out the PR head, the next npm run executes whatever the fork's package.json, test config and dependencies say, with those secrets in the environment. In a Next.js + Supabase repo, the secret is often a staging project's service_role key, which bypasses every RLS policy.

Here's the shape to look for, right after someone hits the error and reaches for the opt-out:

name: e2e
on:
  pull_request_target:
    types: [opened, synchronize, reopened]

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          # added to clear "Refusing to check out fork pull request code"
          allow-unsafe-pr-checkout: true
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run test:e2e
        env:
          SUPABASE_URL: ${{ secrets.STAGING_SUPABASE_URL }}
          SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.STAGING_SERVICE_ROLE_KEY }}

Anyone who forks the repo can change test:e2e to a command that posts the service_role key somewhere, open a PR, and the workflow runs it. Here's GuardLayer's live engine output for that file:

guardlayer scan · .github/workflows/e2e.ymlLive engine output
Passed with warnings
92/100 · A
  • Warning.github/workflows/e2e.yml:12

    Workflow runs untrusted PR code with secrets

    Either switch the trigger to pull_request (fork PRs get no secrets there), or keep pull_request_target but do NOT check out the PR head — split the job so that any step touching untrusted code runs without secrets, and pass results between jobs via artifacts. Do not set allow-unsafe-pr-checkout: true just to clear the "Refusing to check out fork pull request code" error: that turns the check back off.

GuardLayer flags the head-SHA checkout whether or not the flag is set, because allow-unsafe-pr-checkout: true switches GitHub's guard off and leaves the hole in place.

Why GitHub shipped this. GitHub's changelog calls the pwn request "the root cause of multiple supply-chain incidents." For what leaked CI secrets look like at scale, see CVE-2025-30066: per NVD, tags v1 through v45.0.7 of tj-actions/changed-files were repointed on March 14-15, 2025 to a malicious commit that exposed secrets through Actions logs (CVSS 8.6, added to CISA's Known Exploited Vulnerabilities catalog on March 18, 2025). That was a compromised action, not a pwn request, but the target was the same: the secrets a workflow holds, exposed in logs anyone can read on a public repo.

How do I fix "Refusing to check out fork pull request code"?

Switch the trigger to pull_request unless the job needs secrets on fork PRs. Under pull_request, fork PRs get a read-only GITHUB_TOKEN and no other secrets, and actions/checkout checks out the PR's merge commit by default, so you drop the ref: line and the error can't occur.

For Supabase e2e tests, the usual reason for secrets is a hosted staging project. Run against a local stack instead. Supabase's own CI docs use supabase/setup-cli@v1 and supabase start on pull_request:

name: e2e
on:
  pull_request:

permissions:
  contents: read

jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: supabase/setup-cli@v1
        with:
          version: latest
      - run: supabase start
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npm run test:e2e

Point the tests at the URL and keys that supabase status prints. They belong to a throwaway stack that dies with the runner.

If some PRs do need the staging project, gate that job to your own branches. Same-repository PRs already receive secrets under pull_request, so no pull_request_target is needed (Dependabot PRs are the exception: GitHub treats them as if they came from a fork):

  e2e-staging:
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest

Fork PRs skip that job. After review, a maintainer can push the contributor's commits to a branch in the repo and open a PR from it to run the job.

If you need a privileged follow-up, such as a PR comment or a label, do the untrusted work in the pull_request workflow, upload an artifact, and do the privileged part in a workflow_run workflow that only reads it. GitHub's docs say to treat those artifacts as untrusted data, because a fork can control what goes into them.

Is allow-unsafe-pr-checkout: true ever safe?

Only when the checked-out code is never executed, which rules out most jobs. GitHub's docs name npm install and npm run build as commands that can run attacker-controlled code, along with the config files and dependencies the PR brings with it. A test runner or linter that reads its config from the checkout counts too. If your job runs any of them, the flag is the vulnerability. GitHub gave the input a deliberately conspicuous name so reviewers and static analysis can spot it.

Know the guard's limits too. Per GitHub, it doesn't cover git fetch or gh pr checkout in a run: step, issue_comment workflows, or an unrelated third-party repository. GuardLayer's rule has its own: it reads pull_request_target workflows only, so a workflow_run or issue_comment workflow that fetches PR code is out of scope, and it catches a git fetch of refs/pull/.../head but not gh pr checkout.

What changes on December 2, 2026?

GitHub starts enforcing a default rule that disables pull_request_target in public repositories that have no applicable Actions event policy. Private and internal repos are excluded. GitHub's September 17, 2026 changelog added the rule in evaluate mode, and enforcement applies to affected repos that were on the default policy before general availability. An editor's note dated October 21, 2026 updated the enforcement date to December 2, so check the changelog again before you plan around it.

Use the policy Insights view to see which runs would fail, then either let the rule block the trigger or allow pull_request_target in an event policy scoped to specific workflow files. If you moved everything to pull_request, there's nothing to allow.

Quick self-check

# 1. Which workflows run on pull_request_target?
grep -rlE "pull_request_target" .github/workflows/

# 2. Do any of them fetch PR code or opt out of the guard?
grep -nE "head\.sha|head\.ref|merge_commit_sha|refs/pull/|head\.repo\.full_name|allow-unsafe-pr-checkout|gh pr checkout" .github/workflows/*.y*ml

# 3. Which checkout steps are pinned to a SHA or an exact version?
grep -nE "actions/checkout@([0-9a-f]{40}|v[0-9]+\.[0-9])" .github/workflows/*.y*ml

A pin from step 3 only has the guard if it points at v7.0.0 or later, or at one of the July 20 backport releases (v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0) or a later release of the same major. If an outside PR already ran one of the workflows from step 2, treat every secret that job could read as exposed and rotate it, the same playbook as a key pushed to GitHub.

GuardLayer reads workflow files in the same scan as your app code, so a head checkout under pull_request_target shows up next to the items on the Next.js + Supabase security checklist.

FAQ

Why does the error only happen on some pull requests? The guard fires only for PRs from forks. It compares the head repository ID with the base repository's, so PRs from branches in your own repository check out normally.

Does the pull_request event throw this error? No. The check runs only in pull_request_target workflows and in workflow_run workflows started by a pull request event. Under pull_request, fork PRs get no secrets apart from a read-only GITHUB_TOKEN, so checking out their code is the safe default.

Can I pin an older actions/checkout to make the error go away? You can, and it removes the guard, not the risk. Only v7.0.0 and the July 20, 2026 backports (v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0), plus later releases of those majors, carry the check, and @v1 never got it, so an older pin goes back to running fork code with your secrets.

Does this affect private repositories? The checkout guard checks whether the PR comes from a fork, not the repository's visibility. The December 2, 2026 default rule that disables pull_request_target applies only to public repositories.

Does GuardLayer block the workflow? No. It reports the workflow file as a warning in the GitHub check it posts. It doesn't cancel runs, and under the default gate a warning doesn't fail the check. The fix is the workflow change above.

Catch this before it ships — free

Scan your files for this and 35 other Next.js + Supabase issues, with the exact fix inline. Connect GitHub and GuardLayer checks every push too — free on public repos.

No signup, no card. Raw files aren’t kept; only the report is saved.

Keep reading

A Solvion project — see also Reglog — EU AI Act changelog, Proceedly, Solenna and Solvion Solutions.