GitHub Actions Cache Goes Read-Only on Untrusted Triggers

A terminal window showing the GitHub Actions changelog entry describing read-only cache tokens for workflow events that can be triggered without write permissions
On this page

The attack this closes

The Actions cache was read-write for every triggering event before this change, including events anyone can cause without holding write access to the repo. pull_request_target is the clearest example: it runs with the base repo’s permissions, but the event itself can be caused by opening a pull request from a fork, something an outside contributor can always do.

That combination let an external contributor poison a default-branch cache entry, a dependency artifact, a build output, anything a later job would restore and trust. GitHub’s own changelog names the consequence directly:

“a trusted workflow such as push or schedule would later restore” the poisoned entry, closing “a path to run arbitrary code and exfiltrate production secrets.”

The fix, effective 2026-06-26, scopes cache-token write access to trust level of the triggering event rather than treating every event the same.

Which events still get read-write, and which don’t

Trigger typeCache token access after 2026-06-26
pushRead-write
scheduleRead-write
workflow_dispatchRead-write
repository_dispatchRead-write
pull_request_targetRead-only
issue_commentRead-only
Fork-originated workflow_run cascadesRead-only

The line GitHub draws is straightforward:

“…can be triggered without write permissions to the repository.”

Anything an outside contributor can cause on their own, no maintainer action required, loses write access to the default branch’s cache scope. Anything that requires a maintainer or a scheduled job to fire keeps it.

Structural Comparison Matrix

Operational AspectBefore (2026-06-25 and earlier)After (2026-06-26+)
Cache write access from pull_request_targetRead-write, no restrictionRead-only
Cache write access from push/scheduleRead-writeUnchanged, still read-write
Cache poisoning via an untrusted triggerPossibleBlocked: token can’t write
Workflow needing to save cache from a restricted eventSingle workflow handled save and restoreRequires a separate trusted-trigger workflow

Restructuring a workflow that saved cache from an untrusted trigger

If a pull_request_target (or issue_comment, or fork-triggered workflow_run) job in your repo relies on actions/cache’s save action or a post-job automatic save, it now silently stops writing rather than erroring loudly, check for stale cache hits as the symptom, not a clear failure message.

The fix is to split cache population from cache use across two workflows:

.github/workflows/cache-populate.yml
# Trusted trigger: read-write cache access
name: Populate build cache
on:
  push:
    branches: [main]
  schedule:
    - cron: "0 6 * * *"

jobs:
  populate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/cache@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('package-lock.json') }}
      - run: npm ci
.github/workflows/pr-check.yml
# Untrusted trigger: read-only cache access, restore only
name: PR check
on:
  pull_request_target:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: deps-${{ hashFiles('package-lock.json') }}
      - run: npm ci # falls back to a real install on a cache miss

The pull_request_target workflow now only restores, using actions/cache/restore instead of the combined actions/cache action, since it never has permission to write back. The trusted push/schedule workflow owns keeping the cache current.

A cache miss under the new model isn't a bug

If the untrusted-trigger workflow’s cache key doesn’t match anything the trusted workflow already saved, it just runs a real install instead of restoring from cache, the same as any normal cache miss. That’s expected behavior under this model, not a sign something’s broken.

Source

github.blog/changelog: Read-only Actions cache for untrusted triggers (2026-06-26). Browse more posts like this in the Dev Tools archive.

Frequently asked

Does this break my normal push-triggered build cache?

No. push, schedule, workflow_dispatch, and repository_dispatch keep read-write cache tokens on the default branch. Only untrusted-trigger events, ones that can be caused by someone without write access, are restricted to read-only.

What's actually being prevented here?

Cache poisoning followed by trusted execution. Before this change, an untrusted trigger like pull_request_target could write to the default-branch cache scope. A later trusted run (push or schedule) would then restore that same cache entry and could execute whatever was planted in it, a path GitHub's own changelog describes as being able to run arbitrary code and exfiltrate production secrets.

My workflow needs to populate the cache from a restricted context. What do I do now?

Split it into two workflows. Keep a trusted-trigger workflow (push, schedule, workflow_dispatch, or repository_dispatch) dedicated to the cache save step, and let the untrusted-trigger workflow only restore from it. That's a structural change to how the workflow files are organized, not a config flag to flip.

Emitted as FAQPage JSON-LD from the same frontmatter — one source, no duplicated prose.

Recent posts

Full-text search via Pagefind · ↑↓ to navigate · ↵ to open