Fixing empty secrets in GitHub Actions fork pull requests
GitHub Actions withholds secrets from fork pull requests by design. Learn safe patterns like workflow_run to handle credentials without compromising security.
When a continuous integration pipeline passes on internal branches but fails immediately upon receiving a contribution from an external fork, the root cause is often not a configuration error. GitHub Actions intentionally blocks access to repository secrets during these specific events to prevent credential theft. Understanding this security boundary is essential for maintaining robust pipelines in open-source or collaborative environments without exposing sensitive data to untrusted code.
What happened
The core issue arises when a job that functions correctly on standard branches encounters empty credentials during a pull request triggered by a forked repository. This behavior is not a bug or a misconfiguration but a deliberate security feature. GitHub Actions scopes the default token to read-only permissions and completely withholds user-defined secrets from these runs. The system does not provide a setting to override this restriction because doing so would create a critical vulnerability.
The failure manifests subtly. Instead of an explicit error message stating that secrets are missing, the secret variable interpolates into an empty string. Downstream tools then fail with their own generic error codes, such as HTTP 401 Unauthorized or complaints about missing required inputs. This ambiguity often leads engineers to waste time debugging network connectivity or tool versions rather than recognizing the permission model at play. A simple diagnostic step can confirm the issue by checking if the event source is a fork and if the secret value is non-empty, providing immediate clarity.
This design exists because the code executing in a fork pull request is controlled by the contributor, not the repository owner. If secrets were available, a malicious actor could modify a build script or test file to exfiltrate deploy keys and API tokens in a single commit. By drawing the trust boundary at the point of code execution, GitHub ensures that untrusted code never gains access to sensitive environment variables. The only triggers that carry full secrets are those that run in the base repository context, such as specific target events or subsequent workflow runs.
Key details
- GitHub Actions withholds all repository secrets from
pull_requestevents originating from forks. - The
GITHUB_TOKENis automatically scoped to read-only permissions for these events. - Failures appear as generic authentication errors or missing input warnings rather than explicit secret issues.
- The
pull_request_targettrigger provides secrets but checks out base code by default, requiring careful handling to avoid executing untrusted PR code. - Dependabot pull requests also lack access to standard repository secrets, requiring separate configuration in the Dependabot secret store.
- Using
workflow_runallows a second, trusted workflow to access secrets after an initial untrusted build completes.
Background
To understand why this restriction exists, one must recognize the difference between trusted and untrusted code contexts. In a standard branch push, the code belongs to the repository collaborators who already have access to the project resources. In a fork pull request, the code comes from an external user who may not have write access. If the CI system executed this external code with full administrative privileges, it would effectively grant every contributor the ability to steal credentials.
The solution involves separating the build process from the deployment or integration testing phases. The initial phase runs the untrusted code in a sandboxed environment without secrets. Once this phase produces artifacts, such as compiled binaries or test reports, a second phase can consume these artifacts in a trusted context. This separation ensures that while the code is tested, it never directly interacts with sensitive infrastructure credentials unless explicitly approved and isolated.
Why it matters
For teams managing open-source projects or accepting external contributions, ignoring this security model can lead to severe breaches. Attempting to bypass the restriction by using unsafe triggers like pull_request_target with a checkout of the pull request head creates a "pwn request" scenario. In this situation, the CI runner executes arbitrary code from the contributor with full access to all repository secrets. This risk is particularly high in popular public repositories where attackers actively scan for such misconfigurations to harvest cloud credentials.
Conversely, failing to handle this correctly results in broken contributor experiences. When external developers submit pull requests that fail due to missing credentials, they may assume the project is poorly maintained or broken. This friction discourages community participation and increases the maintenance burden on core team members who must manually explain the failure. Implementing clear skip logic or separate trusted workflows ensures that contributors receive accurate feedback while keeping the production environment secure.
Additionally, many teams overlook that some integration tests do not actually require live secrets. By replacing external service calls with local stubs or service containers, teams can run more comprehensive tests within the untrusted fork context. This approach reduces the need for complex multi-workflow setups and speeds up feedback loops for contributors. It shifts the focus from managing secret access to improving test isolation and reliability.
What you can do
- Audit your workflow files to identify steps that genuinely require secrets versus those that can use local stubs or service containers.
- Implement a conditional check in your jobs to skip secret-dependent steps when running on fork pull requests, using a visible notice to explain the skip.
- Use the
workflow_runevent to trigger a second workflow that downloads artifacts from the initial build and performs secret-dependent tasks in a trusted context. - If using
pull_request_target, ensure you never checkout the pull request head SHA; always stick to the base branch code to prevent code injection. - Configure separate secrets for Dependabot if your automated dependency updates fail, as they do not share the main repository secret store.
- Post commit statuses manually from trusted workflows to ensure contributors see the results of secret-dependent checks on their pull requests.



