GitHub Actions cache-mode: Least-Privilege Cache Access Now GA
GitHub made cache-mode generally available on all plans: workflows and jobs can now declare exactly how much Actions cache access they need — read, write, write-only, or none — and the cache service enforces it server-side.
What shipped
Four access levels at both workflow and job scope, with job-level settings overriding workflow-level. Defaults are implicit least-privilege: low-trust events like pull_request_target get read (restores only), trusted events like push get write. Two details deserve attention. First, enforcement lives in the cache service, not the client — a workflow can't escalate by calling a different action. Second, reusable workflows inherit a ceiling: a called workflow can never receive more cache access than its caller granted. That's a capability model, not an advisory one.
What changed
Before this, defending against cache poisoning meant hygiene: unique cache keys per branch, avoiding cache: 'npm' in untrusted contexts, or hand-rolled wrappers around actions/cache that simulated read-only. None of it was enforced — any step with write-implied defaults could save a poisoned tarball under a key a trusted job would later restore. Now the read/write split is structural. The escape hatch — explicitly declaring write or write-only on a pull_request_target event — still exists, but GitHub attaches a warning annotation to it, which turns "who disabled the default?" into an auditable signal. Context: the cache itself is a 10 GB-per-repo attack surface that every job in the repo shares, which is why a single poisoned entry historically propagated to build, test, and release jobs alike.
Why a builder cares
If you run pull_request_target workflows with secrets — the classic pattern for "comment to deploy" bots — your threat model just got one layer cheaper: default read-only cache plus job-level none on the untrusted steps closes the restore-then-exfiltrate path without forking a custom cache action. The caveat GitHub's own docs hint at: write-only sounds safe (can't read a poisoned entry) but still lets a compromised pull-request job plant a payload under a key your release job restores later. Set it none, not write-only, on anything untrusted. This lands the same week GitHub shipped rulesets that block PRs containing exposed secrets — the platform is quietly assembling a default-secure CI posture, one enforced knob at a time.
FAQ
What are the four cache-mode values in GitHub Actions?
read (restore only), write (restore and save), write-only (save only), and none (no cache access). Job-level settings override workflow-level settings, and the mode is enforced by the cache service rather than the workflow runner.
Is cache-mode available on all GitHub plans?
Yes. It is generally available on github.com for all plans as of September 10, 2026. Workflows that don't declare cache-mode keep the existing secure defaults, including read-only cache for low-trust events like pull_request_target.
Does cache-mode prevent cache poisoning completely?
No. It removes accidental write access, but a workflow that explicitly declares write on a low-trust event can still poison the cache — GitHub adds a warning annotation in that case, but does not block it. Fully untrusted steps should use none.