GitHub Actions now gives you an explicit cache access control: cache-mode. It is generally available on all plans and can be set for a workflow or a job. The four values are simple: read restores caches but cannot save, write restores and saves, write-only saves without restoring, and none disables cache access.
The feature is useful for two reasons. It lets you state exactly what a job needs, and it makes the trust boundary around shared cache data visible in the workflow. That matters because a cache restored by a trusted job can contain files created by an earlier run. GitHub's cache-mode announcement and the workflow syntax reference document the current behavior.
The four cache modes
- read: restore existing entries, never save a new one. Best for untrusted or read-only jobs.
- write: restore and save. Use for trusted builds that own the cache population path.
- write-only: save new entries, but do not restore an existing cache. Useful when you want a clean build to seed a cache without consuming old data.
- none: prevent both restores and saves. Use this when a job should not interact with shared cache state at all.
Set read-only access for a pull request job
A pull request from a fork can contain code an outside contributor controls. If that job only needs to use dependencies already cached by a trusted build, make the boundary explicit:
name: Pull request checks
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 testThis job can restore a matching npm cache, but it cannot replace or create one. The trusted push workflow can populate the cache later. A cache miss still lets the job install dependencies normally; it just takes longer.
Keep cache writers on trusted events
Use write only where the workflow code and inputs are trusted. A common split is read for pull requests and write for pushes to the default branch. If the workflow always needs a fresh install before saving, write-only avoids restoring an older entry.
name: Populate dependency cache
on:
push:
branches: [main]
cache-mode: write
jobs:
cache-dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.13'
cache: pip
- run: python -m pip install -r requirements.txtThe cache mode controls what the cache service allows. It does not make restored files trustworthy. Treat cache contents as untrusted input, do not put secrets in a cache, and keep cache keys narrow enough that unrelated branches or operating systems do not share state.
Job-level mode beats workflow-level mode
Set cache-mode at the workflow level when every job shares the same trust model. Set it on a job when one job needs to write and another only needs to restore. The job setting overrides the workflow setting.
cache-mode: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v4
with:
path: ~/.cache/my-tool
key: test-${{ runner.os }}-${{ hashFiles('**/lockfile') }}
seed:
cache-mode: write-only
runs-on: ubuntu-latest
steps:
- uses: actions/cache@v4
with:
path: ~/.cache/my-tool
key: seed-${{ runner.os }}-${{ github.sha }}The example is intentionally simple. In real workflows, choose keys and restore-keys that match the cache action and dependency manager you use. The dependency caching reference explains scope, key matching, and cache security.
Reusable workflows need an explicit cap
A called workflow inherits the cache boundary from its caller. A reusable workflow cannot receive more access than the caller grants. If the caller does not set an explicit mode, a called workflow may request write even when the trigger would otherwise default to read. Set read on the calling job when the called workflow processes untrusted input.
jobs:
external-checks:
uses: acme/ci/.github/workflows/test.yml@v3
with:
node-version: 22
cache-mode: readA practical migration checklist
- Find workflows triggered by pull_request_target, issue_comment, workflow_run, or other events where outside input can influence execution.
- Start with read for jobs that only need dependency restores.
- Keep write or write-only on trusted cache population jobs.
- Set the mode on callers of reusable workflows, not only inside the called workflow.
- Run a pull request and a trusted push, then verify restore and save behavior in the logs.
The smallest useful default is clear: read for untrusted jobs, write for trusted jobs, write-only when you need a clean cache seed, and none when cache state should not be part of the job. Explicit cache-mode makes that decision reviewable and keeps a performance optimization from becoming an accidental trust channel.
