Safeguard
Security Guides•

How do you scan fork pull requests without exposing secrets?

Run the scan on pull_request with no secrets, hand off results as data, and let a second workflow report them. Steps, risks and what to never execute.

Safeguard Security Team
4 min read

Run the scan in a workflow triggered by pull_request, which gives a fork no secrets and a read-only token. Save the results as an artifact. A second workflow, triggered by workflow_run, reads the results as data and posts them. The scanning code never sees credentials, and the reporter never runs contributor code.

Why one workflow is not enough

A scan that comments on the pull request needs a token that can write. Fork pull requests do not get one, so the comment step fails. The tempting fix is pull_request_target, which does have write access, but if that workflow also checks out the contributor's code to scan it, you have handed secrets to untrusted code.

GitHub documents that a workflow started by workflow_run can access secrets and write tokens even when the previous workflow could not. That is the point of the pattern: privilege lives in a workflow that never touches the contributor's code.

The two workflows

Workflow A, untrusted. Trigger: pull_request. Permissions: contents read. It checks out the merge ref, runs the scanner, and uploads the report as an artifact, along with the pull request number.

Workflow B, trusted. Trigger: workflow_run on completion of A. It downloads the artifact, validates it, and posts the result. It does not check out the pull request. It does not execute anything from the artifact.

GitHub notes workflow_run fires if the workflow file exists on the default branch, so the reporter is always the version you merged, not one a contributor edited.

Steps to build it

  1. Write workflow A with a read-only permissions block and no secrets referenced.
  2. Make the scanner output a plain file such as JSON or SARIF, and upload it.
  3. Write workflow B on the default branch with workflow_run, scoped to A by name.
  4. In B, parse the file with a fixed schema. Reject anything unexpected.
  5. Take the pull request number from a validated field, never from free text spliced into a shell command.
  6. Post a comment or a check with just the fields you expect.

What can still go wrong

  • The artifact is attacker influenced. If B treats a field as a command or a file path, the attacker controls B. GitHub's guidance says workflows triggered on workflow_run should treat artifacts from other workflows with caution.
  • B checks out the pull request head to "get the diff". That recreates the original problem.
  • Caches written by privileged triggers can be restored by later runs. GitHub restricts which low-trust triggers can write to the default branch cache, but you should still avoid restoring untrusted caches into jobs with secrets.
  • Chaining. GitHub limits workflow_run chains to three levels, so do not design a long relay.

Try it yourself

Click through it at your own pace, or open it full screen.

Look at the structured findings a scan produces. That is the kind of data workflow B should parse rather than execute.

Where Safeguard fits

Safeguard runs dependency scanning and SBOM generation, with policies that decide on a pull request. Try the free scan on a repository to see the output a reporting workflow would consume.

Quick check

Can the reporting workflow check out the fork code?

It should not. Checking out untrusted code in a privileged workflow is the risk GitHub warns about.

Does the first workflow need any secrets?

No. If a scan needs a credential to run, redesign it so the credential is used in the trusted workflow alone.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.