Build1 publisher3 min readPublished
A workflow_run chain keeps the fork's code out of the job that holds the secrets
GitHub gives forked pull requests a read-only token and no secrets, so any test that needs a credential fails. The two documented ways around that differ in whose code the secrets-bearing job actually checks out.
The Engineer · Build desk

What happened
- GitHub gives a workflow triggered by a forked pull request a read-only GITHUB_TOKEN and no access to secrets, and the dev.to article says the restriction is deliberate.
- The article's checkout snippet for testing the fork's changes passes ref: github.event.pull_request.head.ref, while its surrounding text says the fork's PR head SHA must be set explicitly.
- The pattern it recommends is a pull_request prescan that sees the fork's code with no secrets, followed by a workflow_run job that receives secrets only once the prescan has passed.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure A pull_request_target job pointed at a fork's head runs a stranger's code in the same run as a read/write token and the repository's secrets, so a weak prescan exposes credentials.
- decision Maintainers now choose between a green check that certifies code nobody proposed and a two-workflow chain they have to write, trigger and debug themselves.
- cost The chain is paid for once per repository in a second workflow file plus a trust rule that has to enumerate which head repositories count as trusted, and it has to be maintained as contributors change.
- constraint Until the chain exists, any check that needs a credential cannot report on an outside contribution at all, so reviewers merge fork changes without that signal.
actions/checkout decides whose code the rest of the run executes. It authenticates with GITHUB_TOKEN, fetches the repository at a commit or ref, and places the files into $GITHUB_WORKSPACE so later steps can read them [1]. Under the pull_request trigger, the default is the pull request's head commit [2]. On a fork, that head commit belongs to someone with no permissions in your repository, so GitHub hands the job a read-only token and withholds secrets [3]. Jobs that need a secret fail there, and they fail on purpose [4].
pull_request_target, added in 2021, inverts both halves. The token becomes read/write on forked pull requests, and the context defaults to the base repository [5]. That default is what makes automated labels, comments and project board updates workable [6]. Secret-dependent workflows do run. They run against the base tree, and the dev.to article says this misleads unaware developers with false test results [7].
To aim that job at the proposed change, the fork's head has to be checked out explicitly [8]. Here the article's text and its own snippet ask for different things: the prose says the head SHA must be set, and the snippet passes `ref: ${{ github.event.pull_request.head.ref }}` [8][9]. One names a commit, the other names a branch. The same article lists exploiting race conditions among the ways automated processing of fork pull requests goes wrong [10]. I would pin the SHA the prescan actually examined, and I would keep build steps out of that job entirely. The article puts the boundary in the same place: the primary use case for pull_request_target is non-invasive maintainer tasks on forks, not running or building untrusted code [11].
"Any automated processing of PRs from an external fork is potentially dangerous and such PRs should be treated like untrusted input," Jaroslav Lobačevski of GitHub wrote in Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests [16].
The alternative is two files.
1. A prescan workflow triggered by pull_request. The fork's code is in context, and no secrets are available [13]. 2. On successful completion, workflow_run starts the second workflow. GitHub's documentation says "The workflow started by the workflow_run event is able to access secrets and write tokens, even if the previous workflow was not" [14].
The prescan decides whether the secrets-bearing job ever starts, so its failure conditions are what prevent exposure. The article's example is an author check written with `if` comparisons against the head repository's full name [15]. workflow_run was also introduced in 2021 [12], so both events have been available for the same length of time [18].
The article states there is no foolproof way to allow secrets access on untrusted forked pull requests, and points readers to a repository cataloguing what can go wrong [10][17]. The chain's guarantee is narrower than safety: the job that reads the fork's tree holds no secrets, and the job holding secrets starts only after the prescan passes [13].
What to watch
- Whether the repository the article links for failure modes shows prescan rules that go beyond the head-repository name check.
- Any change to GitHub's default of a read-only token on forked pull requests would remove the reason to build the chain.
- Whether GitHub's pull_request_target warnings begin pointing maintainers at the workflow_run pattern by name.