Trusted triggers such as push, schedule and workflow_dispatch can create or overwrite caches in the default branch scope. Per GitHub's documentation, runs from low-trust triggers like pull_request_target, issue_comment and workflow_run get read-only access to that scope by default, which blocks the classic cache poisoning route.
Why the cache is a security topic
A cache is a bundle of files that a later job unpacks and trusts. If an attacker can write a bad bundle under a key a privileged job will restore, that job runs attacker content with its own secrets. That is cache poisoning.
GitHub's docs describe the risk in these terms: events that outsiders can start, but that run in the default branch context, could write a malicious cache that a later, more privileged workflow restores.
The scope rules
GitHub isolates caches by branch and tag.
- A run can restore caches from its own branch or the default branch.
- A pull request run can also restore from the base branch.
- Runs cannot restore caches from child or sibling branches, or from other tag names.
- A cache created by a pull request run is stored under the merge ref, so only re-runs of that pull request can restore it. The base branch and other pull requests cannot.
So an ordinary pull request cannot plant a cache that your main branch build will pick up. The remaining risk is workflows that run in the default branch scope but are influenced by outsiders.
Who writes by default
| Trigger | Default cache access in the default branch scope |
|---|---|
| push, schedule, workflow_dispatch, repository_dispatch | Read and write |
| pull_request | Not affected, scoped to the merge ref |
| pull_request_target, issue_comment, workflow_run | Read only |
When a read-only run tries to save a cache, the save fails with a warning but the job continues. GitHub suggests keeping the cache warm from a trusted workflow, such as a build triggered by a push to the default branch, and using the restore-only action in low-trust workflows.
Tighten it further with cache-mode
GitHub's docs describe a cache-mode key you can set at workflow or job level. Its values are read, write, write-only and none. Give each job the least it needs. A lint job that only consumes a cache can be read. A job that never needs one can be none.
The docs also include a warning. Explicitly setting write or write-only on a workflow with a low-trust trigger brings back the poisoning risk the default exists to prevent. Prefer a narrower fix, such as keeping the job on read and letting a trusted push workflow maintain the cache.
Practical steps for a small team
- List your workflows that use caching and note their triggers.
- Mark any that run on pull_request_target, issue_comment or workflow_run. Confirm none declares a write capable cache-mode.
- Make deploy and release jobs restore as little as possible. A cold install is slower but removes a trust dependency.
- Key caches on your lockfile hash so a changed dependency set cannot reuse stale content.
- Know the housekeeping rules: unaccessed entries are removed after 7 days, and the default repository limit is 10 GB, with eviction by last access when it is reached.
Where Safeguard fits
Cache scope is a platform setting. Safeguard's SBOM and dependency scanning cover what ends up installed from lockfiles, which helps you notice when a dependency set changes. See also our post on cache poisoning attacks for background.
Quick check
Can a pull request from a fork poison my main branch cache?
Not through a plain pull_request run, because its cache is scoped to the merge ref. The risk is privileged triggers that check out untrusted code.
Should release builds use caches at all?
It is a judgement call. A clean install removes one trust dependency from the most sensitive job, at the cost of build time.
Sources
Last reviewed: 19 October 2026.