GitHub Actions cache-mode: Control Read and Write Access Safely

GitHub Actions cache-mode is now generally available. Learn when to use read, write, write-only, or none, and how to keep untrusted workflows away from cache writes.

A locked cache vault sends data to a pull request build while a blocked arrow stops before entering it

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 test

This 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.txt

The 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: read

A practical migration checklist

  1. Find workflows triggered by pull_request_target, issue_comment, workflow_run, or other events where outside input can influence execution.
  2. Start with read for jobs that only need dependency restores.
  3. Keep write or write-only on trusted cache population jobs.
  4. Set the mode on callers of reusable workflows, not only inside the called workflow.
  5. 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.