Guide · Backend
GitHub's November 2026 Pull Request Target Block Explained: How to Fix Workflows
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- The GitHub Actions pull request target trigger, written pull_request_target, starts an automated job for a pull request and gives it your repository's secrets: its stored passwords and keys. From 2 November 2026, GitHub blocks it by default in public repositories.
- A pwn request is the attack this stops. The job downloads code from a fork, a stranger's copy of your project, and runs it. That code can then reach your secrets.
- The block is part of workflow execution protections, GitHub's allowlists for who and which events may start a job, generally available since 17 September 2026. Until 2 November, the block only records runs it would stop.
- Private and internal repositories are not affected, and a public repository with its own event policy keeps it. Before 2 November, read Policy insights, GitHub's log of would-be blocks, then switch, split or allow each flagged workflow.
- Beginners can stop after the section on fixing a workflow. Engineers get what the actions/checkout v7 guard really checks, tested on fake events, why older exact-version pins skip it, and how the TanStack attack went through the cache.
What goes wrong when a fork pull request runs with your secrets?
A stranger's code can steal your secrets. That happens when a GitHub Actions pull request target workflow downloads code from a fork and runs it. It is why GitHub is changing the default.
GitHub Actions is GitHub's built-in automation. A workflow is a small YAML file, a plain-text settings format, that says which jobs to run when something happens, such as a new pull request. A fork is anyone's personal copy of a public repository.
Picture a common setup. A small team maintains a public open-source Python library, and strangers send pull requests to it every week. The setup is made up, but the problem is real.
Three workflows run on each pull request. The ci.yml file runs the tests. The labeler.yml file adds labels such as docs or tests. The preview.yml file builds the documentation and uploads a preview to the team's cloud bucket.
Two of them, labeler.yml and preview.yml, use the pull_request_target trigger. The preview job also uses DEPLOY_TOKEN, a secret key for the cloud account. Its checkout step is pinned to the commit SHA, the exact ID, of an older release. That detail matters later.
Which words do you need before reading about pwn requests?
You need five words: trigger, secret, GITHUB_TOKEN, base repository and checkout.
- Trigger, also called an event. What starts a workflow, such as a push of new code or a new pull request.
- Secret. A password or key stored in repository or organisation settings, such as DEPLOY_TOKEN. Jobs can use it; visitors cannot see it.
- GITHUB_TOKEN. A key GitHub creates at the start of each job and expires when the job finishes. Depending on its permissions, it can comment, add labels or push code.
- Base repository. The project a pull request wants to change, here the team's library. The fork holding the new code is the head repository.
- Checkout. Copying a repository's files onto the machine running the job, usually with GitHub's official actions/checkout step.
What is the GitHub Actions pull request target trigger?
It is a trigger, written pull_request_target, that runs a pull request workflow in the context of your own repository. The job gets your GITHUB_TOKEN and your repository and organisation secrets, even for a pull request from a fork.
The pull_request trigger is stricter. For fork pull requests to a public repository, the token is read-only and no other secrets reach the job. GitHub Security Lab explains that pull_request_target was added so workflows could label and comment on fork pull requests.
By default, a pull_request_target workflow file and any checkout without a ref come from the base repository's default branch. So no fork code runs on its own. Trouble starts when a workflow deliberately fetches the fork's code.
| Question | pull_request trigger | pull_request_target trigger |
|---|---|---|
| Where the workflow file comes from | The merge commit: the pull request's code as if merged | The base repository's default branch |
| Code checked out by default | The pull request's merge commit | The default branch, not the pull request |
| GITHUB_TOKEN for a fork pull request | Read-only | The base repository's token, with the permissions the workflow grants |
| Other secrets for a fork pull request | Not passed to the job | Repository and organisation secrets are available |
| Blocked by default in public repositories from 2 November 2026 | No | Yes, unless an event policy allows it |
Think of an office reception. With pull_request, a visitor waits in the lobby, and no keys are kept there. With pull_request_target, your receptionist holds the master keys while dealing with the visitor.
The mapping: the visitor is the pull request, the receptionist is the job, and the master keys are your secrets. The receptionist is safe while she only reads the visitor's form. She is unsafe the moment she follows instructions written on it.
Where the picture breaks: a real receptionist can judge a visitor. A job cannot. Any build, install or test command runs whatever files the pull request supplied, with no judgement at all.
What is a pwn request, and how does it steal secrets?
A pwn request is a privileged workflow, such as one on pull_request_target, that checks out a fork's code and then runs it. GitHub's docs say this pattern has caused multiple supply-chain compromises: attacks that poison software other people install.
Pwn is hacker slang for taking over. Pause and predict: the workflow file comes from your default branch, so which step lets a stranger's code in? Read the next paragraph to check.
It is the step after the checkout. Installing dependencies, building and testing all run files the pull request controls, such as a setup script or a test file. GitHub's docs say the attack is completed by the next step that runs the checked-out code.
The team's preview.yml is exactly this pattern. It checks out the pull request head, the latest commit on the fork, and runs the docs build. A later step uploads the result with DEPLOY_TOKEN.
An attacker can add one line to the docs build script that sends data to their server. That script runs inside the privileged job. By default, actions/checkout saves the job's GITHUB_TOKEN on the machine for later git commands, so the script can read it. Any later step that runs files from that folder is the attacker's too.
How a pwn request reaches your secrets
Drag to exploreNotice that the checkout is harmless on its own. The theft happens at the build step, because the build runs the fork's files inside a job that holds keys.
Diagram connections:
- Attacker's fork to Pull request opened (opens)
- Pull request opened to preview.yml starts (triggers)
- preview.yml starts to Checks out the PR head (fetches fork code)
- Checks out the PR head to Runs the docs build (build step)
- Runs the docs build to Keys within reach (reads)
- Keys within reach to Attacker's server (sent out)
- Attacker's fork: Edits the docs build script
- Pull request opened: Fork → base repo, titled as a typo fix
- preview.yml starts: pull_request_target: token and secrets
- Checks out the PR head: Fork files are copied into the job
- Runs the docs build: The attacker's script executes
- Keys within reach: Saved GITHUB_TOKEN, DEPLOY_TOKEN via later steps
- Attacker's server: Receives what the script collects
This is not just theory. TanStack's postmortem says its May 2026 npm compromise began with a pull_request_target workflow. It checked out the fork pull request's merge ref, GitHub's name for its code as if merged, and ran a build. The engineer section below traces that chain.
What changes for public repositories on 2 November 2026?
From 2 November 2026, GitHub enforces a default policy that blocks workflows triggered by pull_request_target in public repositories. Until then it runs in evaluate mode: runs continue, and GitHub records which would be blocked.
The default came with workflow execution protections: allowlists GitHub checks before a run starts. Actor rules say who may trigger workflows. Event rules say which triggers may start them.
The default is an event rule that leaves pull_request_target out. GitHub's changelog scopes the 2 November date to repositories that were using this default before general availability on 17 September 2026.
Which repositories and plans does the default block affect?
Only public repositories without their own applicable event policy. Private and internal repositories are not affected, whatever their plan.
| Your repository | From 2 November 2026 | What to do |
|---|---|---|
| Public, uses pull_request_target, no event policy of its own | Those runs are blocked | Fix or allow each flagged workflow |
| Public, does not use pull_request_target | Nothing visible changes | Keep the block as free protection |
| Public, already has an applicable event policy | Your policy applies, not the default | Check it lists only needed events |
| Private or internal | The default does not apply | Optionally add your own event policy |
The team's repository is public, on the Free plan, with no event policy. So labeler.yml and preview.yml would both stop on 2 November.
How do you find workflows that will break before 2 November?
Open Policy insights and look for runs the default policy would have blocked. Then search your workflow files, because a workflow that has not run lately will not appear there.
Open the policies page
In the repository, click Settings. In the left sidebar, under Actions, click Policies. Organisations have the same page.
Read Policy insights
Policy insights sits directly under the policies page in the sidebar. It lists runs that were blocked, or would have been blocked in evaluate mode.
Search your workflow files
From the repository root, run grep -rl pull_request_target .github/workflows to list every file that uses the trigger.
Fix each workflow before 2 November
Use the decision diagram in the next section, merge the changes, then run the search and check insights again.
For the team, the grep command prints two lines: .github/workflows/labeler.yml and .github/workflows/preview.yml. The short script below goes one step further and flags signs that a job fetches pull request code.
from pathlib import Path
PRIVILEGED = ("pull_request_target", "workflow_run", "issue_comment")
RISKY = (
"github.event.pull_request.head", # the fork's latest commit
"github.head_ref", # the fork's branch name
"refs/pull/", # GitHub's refs for pull request code
"merge_commit_sha",
"workflow_run.head", # the code a workflow_run was started for
"gh pr checkout",
"download-artifact", # files a fork build may have made
"allow-unsafe-pr-checkout", # checkout guard switched off
)
def scan_workflows(repo_dir="."):
folder = Path(repo_dir, ".github", "workflows")
for path in sorted(folder.glob("*.y*ml")):
text = path.read_text(encoding="utf-8")
triggers = [t for t in PRIVILEGED if t in text]
if not triggers:
continue # none of the triggers this script checks
found = [m for m in RISKY if m in text]
level = "REVIEW NOW" if found else "check what it does"
print(f"{path.name}: {', '.join(triggers)}, {level} {found}")
scan_workflows()Save it as scan_workflows.py and run python3 scan_workflows.py from the repository root. On the team's repository it prints exactly these two lines.
labeler.yml: pull_request_target, check what it does []
preview.yml: pull_request_target, REVIEW NOW ['github.event.pull_request.head']What just happened? The script skipped ci.yml, because pull_request gives fork pull requests no secrets. It flagged preview.yml, because that file names the fork's latest commit. It is a text search, not a security scanner, so read every flagged file yourself.
The script also checks issue_comment workflows, which run from your default branch when someone comments on an issue or pull request. GitHub's docs say one that fetches and runs a fork's pull request code is vulnerable in the same way. Neither the 2 November block nor the checkout guard covers them.
How do you fix a workflow that uses the pull_request_target trigger?
Pick one of three fixes: switch to pull_request, split the work into two workflows, or allow one file in an event policy. A job that only tests code needs the first.
Which fix fits each flagged workflow
Drag to exploreFollow the two questions in order. Only a job that never runs pull request code should keep the trigger, and then only through a single-file allowance.
Diagram connections:
- A flagged workflow to Needs secrets or write? (check)
- Needs secrets or write? to Switch to pull_request (No)
- Needs secrets or write? to Runs the PR's code? (Yes)
- Runs the PR's code? to Split into two workflows (Yes)
- Runs the PR's code? to Allow this one file (No, only reads file names)
- A flagged workflow: From Policy insights or the script
- Needs secrets or write?: For pull requests from forks
- Runs the PR's code?: Install, build, test or any script
- Switch to pull_request: Read-only token, no secrets for forks
- Split into two workflows: pull_request builds, workflow_run publishes
- Allow this one file: Event policy with workflow file targeting
Switch to the pull_request trigger when you can
A workflow that only runs tests or style checks does not need secrets. Change its trigger to pull_request, and fork pull requests get a read-only token and no secrets. The team's ci.yml already works this way.
Split privileged work into a second workflow
When you must build untrusted code and then use a secret, separate the two. In GitHub Security Lab's pattern, a pull_request workflow builds the code and saves its output as an artifact: a bundle of files kept with the run.
A second workflow, on the workflow_run trigger, starts when the first one finishes. Security Lab notes it gets write permission and secrets. So it must treat the artifact as plain data, because files built from a fork are untrusted too.
The team rewrites preview.yml this way. The new docs-build.yml builds the pages on pull_request, with no secrets. The new docs-publish.yml downloads them and copies them to the bucket with DEPLOY_TOKEN, without running any of them.
Allow one workflow file with an event policy
Some jobs need pull_request_target but never touch the pull request's code. The team's labeler.yml only reads the list of changed file names. Create an event policy that allows pull_request_target, targeted at that one file.
After the fix, grep prints only .github/workflows/labeler.yml. The script prints the labeler line again, plus docs-publish.yml: workflow_run, REVIEW NOW ['download-artifact']. That flag is expected: check the publish job never runs what it downloads.
Beginners can stop here. The rest is for engineers: what actions/checkout really blocks, why older exact-version pins miss it, and how one real attack used the cache instead of a secret.
What does the actions/checkout v7 guard actually check?
It refuses a checkout when three things hold. The job runs on pull_request_target or workflow_run, the pull request comes from a fork, and the step asks for that pull request's code. We read the rule in the v7.0.1 source and ran it.
The test ran the released dist/index.js file of actions/checkout v7.0.1 against 11 fake event files, with no network. Each commit has a SHA, usually a 40-character ID. A refusal stops the step with an error that begins: Refusing to check out fork pull request code from a 'pull_request_target' workflow. On workflow_run, the quoted name is 'workflow_run'.
| What the step asks for | Result |
|---|---|
| No ref at all, on pull_request_target | Not refused: the guard skips a checkout of your own repository with no ref, which fetches the default branch |
| The pull request's head commit, by SHA | Refused |
| The pull request's merge commit, by SHA | Refused |
| refs/pull/7/merge, GitHub's merge ref | Refused |
| The fork repository by name, any branch | Refused |
| An older commit from the fork, by SHA | Allowed |
| Head commit with allow-unsafe-pr-checkout set to true | Allowed |
| Head commit of a pull request from the same repository | Allowed |
| Head commit on the pull_request trigger | Allowed |
| Head commit on workflow_run after a fork pull_request run | Refused |
| Head commit on workflow_run after a push run | Allowed |
The older-commit row matters. A source comment admits the guard cannot check every fork ref. It compares the request with the fork's name, GitHub's refs/pull names for pull request code, and the head and merge SHAs in the event.
The guard also never sees git fetch or gh pr checkout commands in a run step, or downloaded artifacts. GitHub's changelog names the run-step gap and says other events, such as issue_comment, are not covered. Its docs say to treat artifacts from other workflows as untrusted data. This exact matching is undocumented as of 19 October 2026; pin your version.
Why pinning to an older exact version skips the protection
Since 20 July 2026, GitHub has backported the guard to older major versions except v1. Only floating major tags such as actions/checkout@v4 pick it up. A step pinned to a commit SHA, minor or patch version does not pick it up automatically.
We checked the published files on 19 October 2026. The floating tags v2 to v7 contain the refusal message; v1 does not. Each floating tag from v2 to v6 now points at a new backport release: v2.8.0, v3.7.0, v4.4.0, v5.1.0 or v6.1.0. Earlier exact releases, such as v4.2.2, v6.0.2 and v6.0.3, have no guard.
That is the team's trap: preview.yml pins the commit SHA of an older release, a habit GitHub's own CodeQL check recommends. Pinning is still right. Pin to a v7 commit instead, so you keep the guard and a fixed version.
To check your own repository, run grep -rn actions/checkout@ .github/workflows. A step pinned to v1, or to an exact release or SHA from before the backport, has no guard. Pins to any v7 release or to a backport release have it.
How did the TanStack attack get past read-only permissions?
It went through the shared GitHub Actions cache, not a stored secret. TanStack's postmortem describes a chain of four links, and each link is a place a defence could have stopped it.
The TanStack chain, May 2026
Drag to exploreNotice that no secret was read in the first job. The fork's build poisoned the cache, and a trusted release job later ran the poisoned files.
Diagram connections:
- Fork pull request to bundle-size.yml (triggers)
- bundle-size.yml to Shared pnpm cache (writes poisoned entry)
- Shared pnpm cache to Release workflow (restored later)
- Release workflow to OIDC token in memory (runs attacker binaries)
- OIDC token in memory to npm registry (publishes directly)
- Fork pull request: Hidden script in the fork's code
- bundle-size.yml: pull_request_target builds the merge ref
- Shared pnpm cache: Entry saved under the release job's key
- Release workflow: Push to main restores the cache
- OIDC token in memory: Malware reads it from the runner
- npm registry: 84 bad versions of 42 packages
First, a benchmark workflow on pull_request_target checked out the fork's merge ref and ran a build. Second, that build saved files into the pnpm dependency cache, stored downloaded packages that later jobs reuse, under the key the release workflow used.
TanStack notes that setting the token to contents: read does not block cache writes. Third, a normal push to main started the release workflow, which restored the poisoned entry and ran it.
Fourth, the release job could request an OIDC token, a short-lived identity token used for npm publishing without a stored password. The malware read it from runner memory and published 84 versions across 42 packages on 11 May 2026.
TanStack says no npm tokens were stolen. That is the lesson: the cache, the runner and identity tokens are within reach too, not only named secrets.
GitHub's pull_request_target docs, as of 19 October 2026, say these workflows can restore cache entries from the default branch but cannot create or overwrite them. A save fails with a warning and the job continues. The docs give no start date.
What else can GitHub workflow execution protections control?
They also control who can start workflows and which files a rule covers. Enterprise owners, organisation owners and repository administrators can set them, and lower levels can only add restrictions.
- Actor rules. By default, anyone with write access can trigger workflows. An actor rule can allow only chosen users, roles such as Maintain or Admin, or apps for a targeted workflow.
- Workflow file targeting. Since general availability, a rule can cover specific workflow files instead of a whole repository.
- Require lockfile. This policy blocks workflows whose actions are not pinned to exact commits in a lockfile, .github/workflows/actions.lock. Lockfiles and this policy entered public preview on 14 October 2026.
How do you explain pwn requests in a system design interview?
Frame it as a trust-boundary problem: untrusted code must never run where secrets, tokens or shared caches live.
Name the trust boundary
A fork pull request is input from anyone. Secrets, write tokens and shared caches belong only to reviewed code.
Contrast the two triggers
pull_request runs fork code without secrets. pull_request_target has secrets, so it must never run fork code.
Describe the attack
A pwn request builds fork code under pull_request_target. Build scripts are code, so they reach whatever the job can reach.
Give the layered defence
Default-deny events, single-file allowlists, split workflows, minimal token permissions and a read-only cache each break one link.
Mention the 2026 change
GitHub blocks the trigger by default in public repositories from 2 November 2026, after an evaluate period that began on 17 September.
Questions people ask
- Will my private repository's workflows stop on 2 November 2026?
- No. GitHub's default policy does not apply to private or internal repositories, so their pull_request_target workflows keep running. On GitHub Team or GitHub Enterprise, you can add your own event policy to those repositories if you want the same protection.
- Does the default block stop the pull_request trigger too?
- No. The default policy only leaves out pull_request_target. Workflows on pull_request, push and other triggers keep running unless your own event policy leaves them out. For test jobs, switching to pull_request is the simplest fix.
- Is the pull_request_target trigger always unsafe?
- Not always. It is far safer when the job never runs pull request code and never pastes pull request text, such as the title, into a shell command. A labelling job that reads only changed file names fits this. GitHub's docs say checked-out code must only ever be inspected as data.
- Do I need GitHub Enterprise to allow the trigger again?
- No. Event policies cover all public repositories, and a repository administrator can allow pull_request_target for one workflow file. An organisation or enterprise policy above it can still restrict it. Only evaluate mode for your own policies needs GitHub Enterprise Cloud.
- Does actions/checkout v7 already protect me from pwn requests?
- Partly. It refuses fork pull request code under pull_request_target and workflow_run when you ask for the pull request's refs or latest SHAs. It does not see git fetch, gh pr checkout, artifacts or an older fork commit. The allow-unsafe-pr-checkout input switches it off.
- How will I know a workflow was blocked after 2 November?
- Policy insights lists runs that a policy blocked. As of 19 October 2026, GitHub's docs do not show the exact message a blocked run displays. Check insights after 2 November, and switch, split or allow any workflow you still need.
Sources and further reading
- GitHub Changelog: Workflow execution protections generally available (17 September 2026)
- GitHub Changelog: Control who and what triggers GitHub Actions workflows, public preview (18 June 2026)
- GitHub Docs: Securely using pull_request_target
- GitHub Docs: Control workflow execution
- GitHub Docs: Events that trigger workflows (pull_request and pull_request_target sections)
- GitHub Docs: GITHUB_TOKEN
- GitHub Security Lab: Preventing pwn requests (2021)
- GitHub Changelog: Safer pull_request_target defaults for actions/checkout (18 June 2026, editor's note 15 July)
- actions/checkout v7.0.1 source: unsafe-pr-checkout-helper.ts
- GitHub Changelog: GitHub Actions workflow lockfiles in public preview (14 October 2026)
- GitHub CodeQL query help: Unpinned tag for a non-immutable Action
- TanStack: npm supply chain compromise postmortem (May 2026)
Related guides
- TanStack npm Hack Explained: What JavaScript Developers Must Do NowBackend · 18 min read
- Axios Supply Chain Attack 2026: Malicious npm Versions, Postinstall Malware, and How to Protect Your SystemsBackend · 16 min read
- GitHub's November 2026 App Token Cutoff Explained: Treat Tokens as OpaqueBackend · 13 min read