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
pushorschedulewould 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 type | Cache token access after 2026-06-26 |
|---|---|
push | Read-write |
schedule | Read-write |
workflow_dispatch | Read-write |
repository_dispatch | Read-write |
pull_request_target | Read-only |
issue_comment | Read-only |
Fork-originated workflow_run cascades | Read-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 Aspect | Before (2026-06-25 and earlier) | After (2026-06-26+) |
|---|---|---|
Cache write access from pull_request_target | Read-write, no restriction | Read-only |
Cache write access from push/schedule | Read-write | Unchanged, still read-write |
| Cache poisoning via an untrusted trigger | Possible | Blocked: token can’t write |
| Workflow needing to save cache from a restricted event | Single workflow handled save and restore | Requires 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:
# 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# 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 missThe 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.







