995 words
5 minutes

GitHub Actions Cache Not Saving: Fix Read-Only Trigger Warnings

2026-08-14
DevOps
GitHub Actions
/
CI/CD
/
DevOps
/
Troubleshooting

When GitHub Actions says a cache could not be saved on a pull request, first check the event trust boundary. A cache warning can be expected: GitHub introduced read-only cache tokens for some untrusted triggers so a workflow cannot overwrite a cache used by the default branch. The job can still restore an existing cache, while the save step emits a warning and continues.

The reliable fix is to separate the two responsibilities. Let an untrusted validation workflow restore dependencies, and let a trusted workflow on a controlled branch create or refresh the cache. Do not “fix” the warning by giving untrusted code broader secrets or write permissions.

Read the warning as a trust-boundary signal#

GitHub’s June 26, 2026 changelog says cache tokens can be read-only for untrusted events when the cache scope involves the default branch. The changelog calls out scenarios such as pull_request_target, issue_comment, and fork pull-request workflow_run cascades. It also notes that push, schedule, workflow_dispatch, and several repository-controlled events retain read-write behavior under the documented conditions.

That means “cache not saving” does not always indicate a broken key, a bad path, or an invalid action version. Check these facts before changing YAML:

CheckWhy it matters
Event nameThe token mode can depend on whether the workflow came from a trusted repository event or untrusted contribution.
Pull request originFork and external contribution paths need a stricter cache boundary than a push to a protected branch.
Cache action logA read-only warning is different from a cache miss or a path that does not exist.
Cache key and scopeAn existing cache may be unavailable even when the save operation itself is allowed.

The dependency caching documentation also warns that cache storage can enter a read-only state when usage limits are reached. If the log does not mention an untrusted trigger, check repository cache usage and the key before assuming this new policy is responsible.

Use restore-only behavior for pull requests#

The validation workflow should consume a cache without trying to publish one. The dedicated restore action makes that intent visible:

name: Validate
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Restore dependencies
uses: actions/cache/restore@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- run: npm ci
- run: npm test

This workflow still works when the cache is cold; npm ci downloads dependencies normally. A restore miss is a performance issue, not a reason to grant the pull request permission to write a shared cache.

If your project uses pnpm, replace the path and lockfile expression with the values from your project. The cache boundary is independent of the package manager. The pnpm Git dependency CI guide covers a different pnpm failure path—private Git dependencies—but the same principle applies: keep credentials and write access out of untrusted validation jobs.

Save caches from a trusted workflow#

Create or refresh the cache from a push to a protected branch, or from another event your repository controls. A separate workflow can use the matching key:

name: Warm dependency cache
on:
push:
branches: [main]
jobs:
cache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install dependencies
run: npm ci
- name: Save dependencies
uses: actions/cache/save@v4
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

The key must describe the same dependency state that the validation workflow wants to restore. Include the operating system and lockfile hash when they affect the installed files. Do not assume that every branch can see every cache: GitHub applies branch and event scope rules. Inspect the restore log on a representative pull request and a default-branch push.

A useful refinement is to keep the pull request workflow read-only even when it runs against a branch in the same repository. This prevents a later workflow change, permission expansion, or unusual event path from turning a test job into a cache publisher.

Do not use pull_request_target as a cache shortcut#

pull_request_target runs in the context of the base repository, which makes it tempting to use for write access. That choice is dangerous when the job checks out or executes code from the pull request. GitHub’s secure use guidance for pull_request_target warns against running untrusted code with the elevated context; its example also describes cache-save failure as a warning that does not fail the job.

Keep the concerns separate:

  • Use pull_request for ordinary untrusted validation.
  • Restore only in that validation path.
  • Use a trusted push or controlled maintenance workflow to save caches.
  • Never add secrets or broad write permissions just to suppress a cache warning.

If the warning appeared after a GitHub platform change, record the event name and cache action log in the issue. That evidence distinguishes an intentional read-only token from a malformed path, an exhausted cache budget, or a key that can never match.

Quick diagnosis checklist#

  1. Identify the exact event that started the run.
  2. Check whether the pull request comes from a fork or other untrusted source.
  3. Read the cache step’s warning instead of treating every miss as a save failure.
  4. Confirm the cache path exists after dependency installation.
  5. Confirm the key includes the expected lockfile and runner dimensions.
  6. Keep restore in the pull request workflow and move save to a trusted push.
  7. Re-run one pull request and one default-branch push, then compare both logs.

FAQ#

Q: Does a read-only cache warning fail the workflow?#

A: GitHub documents the restricted save as a warning while cache restore remains available. A separate failing step, such as dependency installation, can still fail the job.

Q: Can a fork pull request save a shared Actions cache?#

A: Do not design the workflow around that. Untrusted trigger paths can receive read-only cache tokens. Restore in the pull request job and save from a trusted branch instead.

Q: Should I add actions/cache/save after every test job?#

A: No. Add saving only where the event and cache scope are trusted. A dedicated default-branch workflow is easier to audit and keeps validation jobs read-only.

Q: What if the warning is actually a cache quota problem?#

A: Check the cache usage and the action log. GitHub also documents read-only behavior when cache limits are reached, so not every warning is caused by an untrusted trigger.

References:

GitHub Actions Cache Not Saving: Fix Read-Only Trigger Warnings
https://laplusda.com/en/posts/github-actions-cache-not-saving-untrusted-trigger/
Author
Zero
Published at
2026-08-14
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.