GitHub Actions cache-mode lets you explicitly separate who may restore caches from who may save them. The feature became generally available on GitHub.com on September 10, 2026 and can be configured for an entire workflow or for a specific job. The goal is straightforward: give each job only the cache capability it actually needs, reducing the cache-poisoning surface without turning every pipeline run into a cache miss.
In practice, the decision is not simply “cache on or off.” There are four levels: read, write, write-only, and none. The most important detail is that the mode also propagates to reusable workflows, and an explicit limit in the caller prevents the called workflow from requesting more cache access than it was granted.
Short answer: which cache-mode should you use?
| Mode | Restore caches? | Save caches? | Typical use |
|---|---|---|---|
| read | Yes | No | Tests, PR validation, and jobs that only need faster installs or builds. |
| write | Yes | Yes | A trusted build that consumes and refreshes cache, usually on pushes to a protected or default branch. |
| write-only | No | Yes | A producer job that publishes a new cache but does not need to trust existing cache content. |
| none | No | No | Deployments, sensitive jobs, or steps where caching adds no value. |
The official definitions are in the workflow syntax documentation and the dependency caching reference. The general-availability announcement also explains the least-privilege and cache-poisoning goals in the GitHub Changelog.
Defaults depend on the trigger
If you do not declare cache-mode, GitHub applies a default based on the event's trust level. Trusted triggers receive effective write access. Low-trust triggers that resolve to the default branch receive read access.
Events that can create or overwrite caches in the default branch scope by default include push, workflow_dispatch, repository_dispatch, delete, registry_package, page_build, and schedule. Other events that resolve to the default branch receive read-only cache access; the documentation lists pull_request_target, issue_comment, and workflow_run as examples.
There is an important exception: pull_request is not affected in the same way because caches created by that event are scoped to the PR merge ref, at refs/pull/.../merge. Those caches cannot be written directly into the default branch scope or reused by unrelated pull requests as though they had been created on the base branch.
Set a restrictive workflow default and open exceptions per job
cache-mode can be set at the top level of a workflow and apply to every job. A value in jobs.<job_id>.cache-mode overrides that default only for the selected job. This lets you start from a conservative policy and grant write access only to jobs that actually maintain the cache.
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
build-main:
if: github.event_name == 'push'
cache-mode: write
runs-on: ubuntu-latest
In this design, every job starts with restore-only access. The build-main job receives restore and save capability only in the path intended to refresh the cache. This reduces the chance that a newly added job silently inherits write access.
Practical matrix: scenario × recommended starting mode
| Scenario | Starting mode | Why |
|---|---|---|
| PR tests that only install dependencies | read | Reuses an existing cache without allowing the job to change it. |
| Trusted build on a push to the default branch | write | Needs to restore the current cache and save a new entry when required. |
| Dedicated cache warmer or publisher | write-only | Avoids trusting an older cache while still allowing a fresh result to be published. |
| Deployment or elevated-credential job with no cache need | none | Removes a shared-state surface that provides no benefit to the task. |
| pull_request_target, issue_comment, or workflow_run with third-party-influenced input | read | Preserves the secure default and avoids reintroducing cache-poisoning capability. |
| Job that calls a reusable workflow | Set the limit explicitly | An explicit caller value caps what the reusable workflow can request. |
This matrix is a starting point. The right question for each job is whether it needs to read, write, do both, or do neither. The best mode is the smallest set of capabilities that still supports the job.
Forks and pull_request_target: where risk increases
The problem cache-mode helps contain matters most when untrusted code or data can influence what is saved. In a cache-poisoning attack, attacker-controlled content can be placed into a cache and later restored by a more privileged workflow.
That is why explicitly declaring write or write-only on low-trust triggers can bypass the read-only default. GitHub warns about this case. If a pull_request_target job processes fork code, PR-controlled data, or other attacker-influenced input before saving cache, write access should be treated as a high-risk exception.
- Prefer cache-mode: read in the low-trust workflow.
- Keep the cache fresh from a trusted workflow triggered by push on the default branch.
- If a job only needs restoration, consider an explicitly restore-only operation such as actions/cache/restore to make the intent clear.
- Never put secrets, tokens, or credentials in cached paths; anyone who can read the cache may extract its contents.
- Treat restored cache files as untrusted input, especially before executing them in jobs with higher privileges.
Reusable workflows: the cap must be explicit in the caller
Reusable workflows require special attention because there is a difference between a trigger-derived default and an explicitly declared limit. cache-mode propagates to the called workflow, and an explicit value on the calling job — or one explicitly inherited from the caller workflow — limits the maximum cache access the reusable workflow may request.
However, if the calling job does not set or inherit an explicit cache-mode, a reusable workflow may explicitly request write even when the low-trust caller trigger would otherwise default to read. To enforce a real ceiling, set the mode on the job that performs the call.
jobs:
shared-tests:
cache-mode: read
uses: org/repo/.github/workflows/tests.yml@v1
If the called workflow requests a capability beyond the explicit limit, the run does not start and GitHub reports a validation error. This also applies to combinations that are not a simple hierarchy: read and write-only grant different, non-overlapping capabilities, so a caller limited to one does not automatically authorize the other. The behavior is documented in reusing workflows.
write-only is useful when you want to produce without consuming
write-only is the least obvious mode and fits a specific pipeline pattern: jobs that generate trusted derived content for a cache but do not need to restore anything first. Blocking reads reduces dependence on prior cached state while still allowing the job's output to be published for future runs.
It is not “a more restrictive write” on a simple read → write ladder. It is a different capability: save without restore. That is why, for reusable workflows, a request for read does not fit inside a write-only limit, and vice versa.
What happens when the mode forbids an operation
The runner exposes the effective mode through the ACTIONS_CACHE_MODE environment variable. The actions/cache action and the @actions/cache toolkit honor it. When reads are not allowed, restore is skipped and treated as a cache miss. When writes are not allowed, save is not performed. Skipped cache operations do not need to fail the job: the run continues and the log records the behavior.
This helps migration because you can apply a tighter policy and observe which jobs were actually attempting to write. Still, do not rely only on warnings or logs: the best configuration states the intent directly in YAML.
Migration strategy without breaking cache behavior
- Inventory who restores and who saves. Search for actions/cache, actions/cache/restore, actions/cache/save, and setup actions that manage cache automatically.
- Start with a restrictive default. In many pipelines, workflow-level cache-mode: read is a useful starting point.
- Grant write only to cache maintainers. These are usually a small number of build jobs on trusted events.
- Use none where cache is unnecessary. Deployments and administrative jobs are common candidates.
- Review low-trust events. Pay special attention to pull_request_target, issue_comment, and workflow_run.
- Pin the limit at reusable-workflow call sites. Do not rely only on the trigger-derived default; declare the ceiling on the calling job.
- Test both cache hits and misses. Confirm that read-only jobs remain fast and that at least one trusted path keeps necessary keys fresh.
A lean policy example for CI and deployment
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
cache-builder:
if: github.event_name == 'push'
cache-mode: write-only
runs-on: ubuntu-latest
deploy:
cache-mode: none
runs-on: ubuntu-latest
This example illustrates three different roles: a consumer-only test, a producer-only cache builder, and a deployment with no cache access. In another project, the builder may need write instead of write-only so it can restore existing dependencies before refreshing the cache. The correct choice follows the actual data flow, not a universal rule.
Practical least-privilege rule for cache access
Use read as the ceiling for jobs that only benefit from existing content; write only when a job truly needs to consume and refresh the cache; write-only when it should publish without trusting previous state; and none when caching is not part of the task. For reusable workflows, turn that ceiling into an explicit caller setting.
The benefit is not only “more security.” A clear policy also makes the pipeline easier to review: someone reading the YAML can see, before inspecting every step, which jobs are allowed to influence shared cache state.





