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
- Write workflow A with a read-only permissions block and no secrets referenced.
- Make the scanner output a plain file such as JSON or SARIF, and upload it.
- Write workflow B on the default branch with workflow_run, scoped to A by name.
- In B, parse the file with a fixed schema. Reject anything unexpected.
- Take the pull request number from a validated field, never from free text spliced into a shell command.
- 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
- Events that trigger workflows, GitHub Docs
- Secure use reference, GitHub Docs
- Dependency caching reference, GitHub Docs
Last reviewed: 19 October 2026.