pull_request_target runs in the BASE repo's context with the BASE repo's secrets — that's the threat model. Two combinations are forbidden:
- Checkout fork code + execute it.
actions/checkoutof${{ github.event.pull_request.head.* }}followed by any step that runs the checked-out code (pnpm i,npm i,pnpm build,cargo build,make,node scripts/*, etc.) gives the fork's PR author arbitrary code execution in a privileged context. They can exfil the workflow's secrets via the runner. - Even without execution, fork content can shape the workflow. A fork's
package.jsonscripts.preinstallor a fork-modified.npmrcruns duringpnpm i. Treat all fork-supplied files as untrusted input.
- A
pull_requestworkflow does the build in the fork's context (no BASE secrets). - It uploads the result as an artifact (
actions/upload-artifact). - A
workflow_runworkflow (triggered by the prior workflow's completion) downloads the artifact, optionally re-signs it, and posts the PR comment with the BASE-repo token.
Crucially: the workflow_run step does not check out fork code. It only consumes the artifact produced by the unprivileged build.
If you genuinely need pull_request_target semantics (e.g. to access a secret-driven comment-poster), gate it on types: [labeled] so only a maintainer who manually labels the PR can trigger the privileged run. This shifts the threat model to maintainer review: they MUST read the diff before applying the label.
The .claude/hooks/pull-request-target-guard/ hook scans workflow YAML for the combo and blocks edits that introduce it. The hook is byte-identical across fleet repos; the rule is the contract, the hook is the enforcer.