GitHub Actions added the cache-mode YAML key on September 10, 2026, with four values: read, write, write-only, and none. The practical answer is to give pull request jobs read, reserve write for trusted branch jobs, use write-only for dedicated cache seeders, and choose none when a job must not consume or publish shared cache state. That is more precise than relying on event-based defaults, but an incorrect override can still grant cache writes to a low-trust workflow.
What GitHub Actions cache-mode changes
Before cache-mode, cache permissions were mostly implicit. GitHub derived them from the workflow trigger and token context. A pull request might restore an existing cache but be unable to save a new entry, while a trusted push could do both. The behavior was defensible, but it was difficult to see during review.
GitHub's general availability announcement makes that policy explicit in workflow YAML. A top-level setting applies to every job. A job-level value overrides it for that job.
read
Restore: Yes. Save: No.
Use for pull request checks that benefit from trusted caches.
write
Restore: Yes. Save: Yes.
Use for trusted pushes that update and reuse caches.
write-only
Restore: No. Save: Yes.
Use for isolated seed jobs that publish freshly built cache data.
none
Restore: No. Save: No.
Use for reproducibility tests and jobs that should avoid shared state.
Our position is simple: set cache-mode explicitly for any workflow where cache state affects build time, provenance, or failure analysis. Defaults remain useful for small repositories, but implicit access is harder to review and easier to misunderstand.
How cache-mode is enforced
The setting is not a hint passed only to actions/cache. GitHub says the Actions service issues cache tokens scoped to the selected mode. Cache actions then encounter permitted or denied operations at the cache service.
Denied operations do not fail the job. A blocked restore behaves like a cache miss, and a blocked save is skipped with an informational log message. That compatibility choice is also the main operational catch. A mistaken read or none value can turn a two-minute job into a fifteen-minute job without making the workflow red.
Shared Actions cache
Cache service token scopes each operation
Pull request tests read
Restore allowed. Saving is denied.
Main branch build write
Restore and save are allowed.
Cache seed job write-only
Save is allowed. Restore is denied.
Reproducibility job none
No shared cache access.
The workflow syntax documentation also defines a ceiling for reusable workflows: a called workflow cannot gain more cache access than its caller. That prevents a reusable workflow from turning a read-only calling context into a cache writer.
Start with a workflow-level read policy
For a repository that runs tests on pull requests, a safe migration starts with a workflow-wide read value. Jobs can restore trusted dependency caches, but they cannot publish new cache entries.
name: pull-request
"on":
pull_request:
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
We parsed this example as YAML and kept the on key quoted so general-purpose YAML 1.1 parsers do not reinterpret it as a Boolean. GitHub accepts the quoted form.
The important design choice is not Node.js or npm. It is the placement of cache-mode beside on and jobs, which makes read the reviewable default for every job in the file. If a future job needs broader access, its exception is visible in that job.
If your team is moving several CI workflows toward explicit cache rules, this is where Axentia's SaaS development work is relevant: the valuable part is mapping trust boundaries and build economics before changing dozens of YAML files, not merely adding one key.
Use write-only for a cache seeder
write-only is the mode that changes the architecture most. It lets a job save a cache without restoring one first. GitHub added this option after developers asked for it in the public cache access design discussion, which collected detailed feedback about cache poisoning, visibility, and reusable workflows before general availability.
A dedicated seeder can rebuild dependencies from the lockfile and publish the result without consuming existing shared cache state:
name: seed-node-cache
"on":
push:
branches: [main]
cache-mode: write-only
jobs:
seed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- uses: actions/cache/save@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
This pattern gives up the speed of restoring an older cache inside the seeder. In exchange, the job publishes state built from the current lockfile and network source rather than state it just downloaded from the same cache service.
Do not use write-only as a decorative hardening flag on every push. A cache seeder that downloads a large dependency graph on every run can cost more runner time than it saves elsewhere. Measure total runner minutes and downstream cache hit rate.
Job-level overrides need a narrow reason
GitHub supports a workflow default plus per-job exceptions:
name: main
"on":
push:
branches: [main]
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo "Tests may restore, but cannot save"
build-cache:
cache-mode: write-only
runs-on: ubuntu-latest
steps:
- run: echo "This job may save, but cannot restore"
verify-clean-build:
cache-mode: none
runs-on: ubuntu-latest
steps:
- run: echo "This job has no cache access"
We would require a short comment or review rule for every broader job override. A top-level read policy is not meaningful if any copied job can quietly switch itself to write.
GitHub warns when write or write-only is explicitly assigned to a low-trust event such as pull_request_target. The warning is useful, but it does not stop the workflow. Treat the annotation as a guardrail, not an enforcement policy.
This matters because pull_request_target runs in the context of the base repository. It is often chosen so automation can label or comment on a pull request. Combining that trigger with a write-capable cache and untrusted pull request code creates a boundary worth reviewing line by line.
Migration checklist for existing workflows
- Inventory workflows that use
actions/cache,actions/setup-node, or another setup action with automatic caching. - Classify each trigger as low-trust or trusted. Pull request code and fork contributions should not publish shared cache state.
- Add a workflow-level mode first. Prefer
readwhen one file mixes jobs with different responsibilities. - Add job overrides only for dedicated writers or clean-build verification.
- Record baseline duration and cache hit rate before the change.
- Search logs for skipped restore or save operations after rollout.
- Test reusable workflows from both read-only and write-capable callers.
This checklist is deliberately operational. The cache service does not fail a job when access is denied, so a green check does not prove that caching still works. Compare median job duration and cache-hit messages for several runs.
The same lesson appears in our Cloudflare Workers 64 MiB limit guide: when a platform changes the unit or policy behind a CI gate, update the measurement and the review rule together.
When cache-mode is not worth adding
Do not add explicit modes to a tiny workflow that has no cache steps and finishes in seconds. The setting would communicate a policy that currently changes nothing.
It is also the wrong solution when the data is a release artifact. Use an artifact store, package registry, or immutable object with a digest when downstream jobs require an exact build output. An Actions cache is optimized for reusable acceleration data, not durable release provenance.
Finally, cache-mode is not an organization-wide policy engine. It lives in workflow YAML controlled by repository contributors. Organizations that need central enforcement still need repository rules, workflow review, reusable workflows with constrained interfaces, or external policy checks.
What to do next
Start with one high-traffic workflow. Set cache-mode: read at the workflow level, isolate cache publication in a trusted job, and use write-only only if a clean seeder improves the trust model enough to justify its runner cost.
Then watch the logs and timing. The best configuration is not the one with the strictest label. It is the one that makes cache access obvious, prevents low-trust writes, and preserves enough cache value to reduce total CI time.
Frequently asked questions
What is the default GitHub Actions cache-mode?
GitHub derives the default from the workflow trigger and trust context. Low-trust events generally receive read access, while trusted pushes can receive write access. Explicit YAML is preferable when reviewers need a stable, visible policy. GitHub documents the exact behavior in its workflow syntax reference.
Does cache-mode work with actions/setup-node caching?
Yes. GitHub enforces cache-mode through scoped cache service tokens, so it applies to setup actions that use the Actions cache service as well as direct actions/cache steps. A denied restore becomes a cache miss, and a denied save is skipped rather than failing the job.
Can a reusable workflow override cache-mode?
A reusable workflow may request a mode, but it cannot gain more cache access than the calling workflow provides. That ceiling is useful for central workflow libraries. Test the reusable workflow from callers with different modes because a denied cache operation remains informational and may only appear as slower execution.
