View on GitHub

Agentic Workflows — Field Guide

A field guide to using GitHub Workflows and Agentic Workflows

← Previous: pull_request Common Triggers Next: The “Approve and run workflows” Gate →

pull_request_target

⛔ Avoid (public repos)
☢️ Use with extreme caution (private repos)

In private repositories, this trigger is ☢️ Use with extreme caution. There are no outside contributors, so the fork-attacker vector and the “Approve and run workflows” button do not apply. However, the full-secrets footgun remains regardless of repo visibility — checking out PR head code in a job with secrets is dangerous even when all contributors are trusted. The trigger is elevated to ☢️ (rather than fully recommended) for defense-in-depth and to maintain consistent practices across repositories.

pull_request_target is the trigger most likely to get a repo pwned, and the approve-and-run gate is the booby trap that springs it. pull_request from forks runs sandboxed (read-only token, no secrets). Maintainers reach for pull_request_target to get write tokens and secrets back.

The pwn vector: it checks out the base ref by default, but real workflows often use actions/checkout with ref: $ to see the PR’s code — at which point the PR code executes with full repo secrets and write GITHUB_TOKEN. Even without checking out PR code, the PR title/body are attacker-controlled prose the agent ingests — instant prompt injection with write tokens.

The approve-and-run gate is what makes this catastrophic, not what protects from it. The approval requirement is on by policy and external contributors must work from forks — so every pull_request_target workflow on every external PR presents the “Approve and run workflows” button, every day. That is alert fatigue by construction: the click-rate trends toward 100%, the UI does not show what is about to execute or give guidance for what to review before approving, and the button label conceals what is actually being authorized (“run safely-defined workflows” reads as “rubber-stamp this routine PR,” not “grant this stranger’s code access to every secret”). The gate is also one-shot per click — approving all gated workflow runs in that batch at once, with no granular visibility or selection of which individual workflows are safe to run for this specific PR. Click once and every gated workflow on the PR fires. gh-aw’s safe-outputs gate, output sanitization, and staged: true exist largely because of this trigger’s footgun shape.

It is possible to use pull_request_target safely, but it’s far easier to get it wrong than to get it right. This should be the last event trigger to consider under all circumstances.

See also: The “Approve and run workflows” Gate, and the Trigger-by-Trigger Risk Profile row for pull_request_target.


Scenarios

Why ⛔: Runs on the base ref with full GITHUB_TOKEN and access to all secrets, even for fork PRs. This is the trigger most likely to get a repo pwned. The trigger can be safely used when extreme care is taken, but the approval gate compounds the danger — clicking approves all gated workflows, including this one with full secrets. The button’s existence creates alert fatigue that undermines the security model. Same as pull_request, the button is presented even if the contributor doesn’t match on.roles: and the workflow will immediately exit.

Recommended alternatives:

Profile

Dimension Recommendation
on.roles: [admin, maintainer, write] (default). Can use triage or all, but with ☢️ extreme caution — only if the workflow is safe taking the same action for a read-only user’s PR as it would for a maintainer-submitted PR.
Activity types Narrowest possible. [opened] or [labeled] for metadata operations. synchronize is valid when you need to reprocess on head-change (e.g., clearing “author action needed” state), but understand it fires on every push. Avoid edited (same re-execution risk as issues.edited, but now with secrets).
Concurrency $-$. cancel-in-progress depends on idempotency considerations and whether output from previous runs would be discarded/overwritten.
Idempotency Required. Every mutation must be safe to repeat — partial-write from a canceled run + retry must converge to the same end state. Note that several synchronize events commonly fire in quick succession (force-push, rebase, rapid commits), which is another reason why a schedule or other trigger is preferred over immediate invocation.
Fork posture Apply if: $ to prevent running within a user’s fork. Additionally, gate the agent step on github.event.pull_request.head.repo.fork == false if the workflow should refuse cross-fork PRs entirely. Never check out the PR head SHA in any job that has secrets in its environment.
Approval gate Subject — and this is where the gate is most dangerous. Clicking approves all gated workflows, including this one with full secrets. Assume the button will always be clicked — by someone trying to approve a different workflow.
Copilot events Copilot-authored PRs are subject to the “Approve and run workflows” approval gate, which teaches team members to click the button when they see it.
Sanitize payload? Yes, critically in pre-agent steps. PR title, body, branch name, label names, and commit messages are all attacker-controlled; use steps.sanitized.outputs.text, never raw $. However, it is acceptable to handle the unsanitized payload within the agent job itself, as that job is sandboxed — this must be coupled with proper safe-outputs handling so the agent cannot exfiltrate or write beyond its declared outputs.
Safe-outputs add-labels, add-comment only. Avoid push-to-pull-request-branch, create-pull-request, update-issue — each is a write amplifier with full secrets.
Integrity filtering approved. Fork contributors are typically CONTRIBUTOR or lower, so their PR content is filtered out before the agent sees it. Integrity filtering does not protect against checking out the PR head SHA — that is a runner-filesystem concern, not a content-trust concern. See standard guidance.

← Previous: pull_request Common Triggers Next: The “Approve and run workflows” Gate →