From 2 December 2026, GitHub Actions will block pull_request_target workflows by default in every public repository. The trigger runs in your base repository's context, with access to its secrets, so a workflow that checks out and runs code from a fork can leak them. Two rehearsal brownouts, on 9 and 16 November, switch the block on for two hours each — your window to find out whether yours are affected.
Key takeaways
The default block lands on 2 December 2026; rehearsal brownouts run on 9 and 16 November, 16:00–18:00 UTC (17:00–19:00 CET, 21:00–23:00 PKT).
Only public repositories on GitHub Free, Team and Enterprise Cloud are affected. Private, internal and GitHub Enterprise Server repositories are not.
The trigger itself is not the vulnerability. Checking out and executing fork code under it is.
We audited 202 workflows across 5 major public repositories: 27 use the trigger, and after manual review none of them executes fork code.
You can audit your own repositories in about 10 minutes with the script below — do it before Monday, not after the first brownout breaks something.
What actually changes on 9 November?
GitHub announced the change on 2 November 2026, and at the same time pushed enforcement back a month, from 2 November to 2 December. The delay is the good news: the rule currently runs in evaluate mode, which means GitHub logs which workflow runs it would block without blocking them. You can see those would-be blocks in your repository's Insights tab before anything breaks.
The brownouts are the rehearsal. On Monday 9 November and again on Monday 16 November, between 16:00 and 18:00 UTC, GitHub will temporarily enforce the block. Any pull_request_target workflow in an affected public repository will fail with an error annotation and will not execute. Outside those windows the rule returns to evaluate mode. On 2 December the block becomes permanent unless an Actions event policy explicitly allows the trigger — GitHub suggests using workflow file targeting to allow specific workflows rather than the trigger in general.
Why is pull_request_target dangerous?
A normal pull_request event from a fork runs with a read-only token and receives no repository secrets, so even malicious fork code has nothing valuable to steal. pull_request_target is the exception: it runs in the base repository's context, with secrets and a write token, so maintainers can run bots that label, comment on or triage fork pull requests.
The danger appears when a workflow combines that privileged context with untrusted code. The classic shape looks like this:
on:
pull_request_target:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm test
env:
SECRET_KEY: ${{ secrets.SECRET_KEY }}
A contributor opens a pull request that quietly edits package.json so npm ci runs a post-install script. The workflow checks out the fork's code, runs it, and the script exfiltrates SECRET_KEY to an external server. The workflow file is trusted — it lives on the base branch — but everything it executes comes from the fork.
The safe shape keeps the privileged trigger but never executes fork code under it:
on:
pull_request_target:
types: [labeled]
jobs:
triage:
runs-on: ubuntu-latest
steps:
# No checkout of the PR head, no run steps.
# Only the workflow's own trusted code and the API.
- uses: actions/github-script@v7
with:
script: |
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ['needs-review']
})
Rule of thumb: under pull_request_target, treat everything from the pull request as untrusted input. To run the fork's code, use a separate pull_request workflow, or the pull_request plus workflow_run pattern, where the unprivileged workflow runs the code and the privileged one acts on its artefacts after a maintainer approves.
What did we find when we audited 202 workflows?
Headlines about a security change are cheap, so we measured instead. We wrote a small Python script that lists every workflow file in a public repository through the GitHub API, fetches each one, and flags pull_request_target use plus the dangerous combination of checking out the pull-request head and running code. We ran it on 6 November 2026 against five large public repositories:
RepositoryWorkflowsUses pull_request_targetExecutes fork code
github/docs101180*
nodejs/node4550
facebook/react2640
python/cpython2300
actions/checkout700
Total202270
Thirteen per cent of workflows use the trigger, and almost all of them use it the way GitHub intended: labelling, commenting, triaging and notifying — bots that need a write token on fork pull requests but never run the fork's code. Node's five are all label and comment bots; React's four are Discord notifications and a core-team label bot.
The asterisk matters. Our script flagged one workflow, review-comment.yml in github/docs, because it mentions the pull-request head SHA and contains run: steps. Manual review showed a false positive: the workflow checks out the base repository without a ref override, so it runs the base branch's code and passes the head SHA only as data. An automated scan is a starting point, not a verdict — every flagged workflow deserves a two-minute read.
We also audited our own repository, logicstack, which publishes this site: 3 workflows, none using pull_request_target or even pull_request. The publish pipeline triggers on pushes to main, so the December change does not affect it at all.
The honest conclusion is narrower than the headline: 87 per cent of the workflows we scanned do not use the trigger at all. The block will break the small minority that depend on it, and the risk concentrates in the few that also execute fork code. Find those before Monday.
How do you audit your own repos in 10 minutes?
Two checks, both quick. First, use GitHub's own signal: open your repository's Insights tab and look for the workflow execution protection findings. The rule has been running in evaluate mode, so GitHub can show you exactly which runs it would have blocked. Do this before 9 November — the brownout is a much louder way to learn the same thing.
Second, scan the workflow files directly. The script below does the same scan we ran above, against any public repository, with no authentication:
import json, sys, urllib.request
API = "https://api.github.com/repos/{}/git/trees/{}?recursive=1"
RAW = "https://raw.githubusercontent.com/{}/{}/{}"
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "audit"})
with urllib.request.urlopen(req, timeout=30) as r:
return r.read().decode("utf-8", "replace")
for repo in sys.argv[1:]:
meta = json.loads(get(f"https://api.github.com/repos/{repo}"))
tree = json.loads(get(API.format(repo, meta["default_branch"])))
wfs = [t["path"] for t in tree["tree"]
if t["path"].startswith(".github/workflows/")
and t["path"].endswith((".yml", ".yaml"))]
for path in wfs:
text = get(RAW.format(repo, meta["default_branch"], path))
if "pull_request_target" in text:
danger = ("pull_request.head.sha" in text
or "pull_request.head.ref" in text) and "run:" in text
print(f"{repo} :: {path} :: REVIEW" +
(" -- checks out PR head + runs code" if danger else ""))
Run it as python3 audit.py owner/repo for each repository you maintain. For private repositories, add a personal access token to the request headers. If it prints nothing, you are done. If it prints a workflow, read that file first; as our github/docs false positive showed, a match is a lead, not a verdict. A faster local version, if you have the repository cloned, is one grep: grep -rn "pull_request_target" .github/workflows/.
How do you fix a workflow that really needs the trigger?
There are three honest options, in order of preference.
1. Remove the trigger. Most workflows that use pull_request_target do not need it. If the workflow only runs tests or builds the fork's code, pull_request is the correct trigger and works without secrets. If it also needs to comment or label afterwards, split it: a pull_request workflow runs the untrusted code with no secrets, and a workflow_run workflow, triggered when the first completes, does the privileged commenting. This removes the risk entirely.
2. Keep the trigger, remove the execution. If the workflow genuinely needs a write token on fork pull requests — labelling, triage, welcome comments — keep pull_request_target but never check out the pull-request head and never run code from the fork. Check out the base branch only, or nothing at all, and treat every field from the pull request as untrusted data. The github/docs and Node.js bots we audited are the model here.
3. Explicitly allow the trigger. If a workflow cannot be restructured before December, GitHub lets you allow pull_request_target in an Actions event policy, with workflow file targeting so you allow specific workflows rather than the trigger everywhere. Treat it as a deliberate, visible exception, with a note explaining why that workflow needs it and when it will be reviewed.
Do not allowlist the trigger repository-wide just to make the warnings go away. That restores the exact default GitHub is removing, and hides the next vulnerable workflow someone adds.
What should you do before Monday?
The brownout on 9 November is a deadline with a built-in test. If a workflow you depend on is affected, find out from Insights today — not from a failed run at 16:00 UTC on Monday while a contributor waits for a label.
Check Insights for would-be blocks in every public repository you maintain.
Run the grep or the script above over your workflow files, and read every flagged file.
For each flagged workflow, decide: remove the trigger, remove the execution, or explicitly allow that one workflow file.
If you allowlist, use workflow file targeting and write down why.
Watch the 9 November brownout window, 16:00–18:00 UTC, for anything you missed — then fix it before the second window on 16 November, and well before 2 December.
GitHub gave everyone a month's warning and two rehearsals. The repositories that break on Monday will be the ones nobody audited.
Originally published on LogicStack. Run the code examples right in your browser with our free interactive Python and SQL playgrounds — no setup, no sign-up.
Top comments (0)