KEY TAKEAWAYS

  • A lockfile-derived key and an exact cache-hit establish a lookup match, not independent verification of the cached bytes.[1]
  • Review every eligible producer in the reachable branch/ref scope. Different workflow filenames do not isolate caches.[1]
  • Choose restore and save capabilities separately, explicitly cap reusable callers, and decline privileged consumption when the producer-to-consumer trust path is unresolved.[1][2][3]

Decide what will use the restored files

Consider a hypothetical release job that restores a dependency cache, sees cache-hit=true, then builds with publishing credentials. That sequence raises a question the hit cannot answer: which workflows could have supplied files that the release job will use? GitHub's dependency-caching reference states that cache contents are not signed or verified and that extracted files may later be executed.[1] Treat the restore as an input to the job, not as a trust check.

The producer is the workflow that saves files; the consumer is the workflow that restores and uses them. Review the complete path between them: eligible writers, cache scope and matching rules, destination paths, and the consumer's authority when those files are first used. This is especially useful when an agent proposes caching as a routine CI speed improvement: the change can affect what a privileged job consumes without changing its credential settings.

A package download cache can be useful without becoming an approved build output or a secret store. The recommendation here is to allow only the cache capabilities a job needs and to withhold a privileged restore when its producer path has not been established. This does not require removing every cache; it requires reviewing the cache the consumer can actually select.

Understand what a cache match establishes

The cache action searches the current branch for the primary key and cache version, then primary-key prefix matches, then restore-keys in their declared order. If that search fails, it repeats against the default branch, subject to access restrictions.[1] An exact primary-key match sets cache-hit to true; a non-exact restoration can still supply files without that exact-hit result.[1] Review fallback selection even when your workflow treats a miss as a reason to install dependencies.

Selection signals and their limits, as documented by GitHub.[1]
SignalWhat it establishesWhat remains to inspect
Exact primary-key hitThe requested key matched an eligible cache with a compatible cache version.The producer and the trustworthiness of the restored files.
Prefix or restore-keys matchA fallback entry can be restored; restore-key prefixes are tried in order, with the newest partial match selected.Which additional producers and dependency states the fallback admits.
Cache versionPath and compression metadata identify an entry the consumer can decompress and use.Producer provenance; compatibility is not authentication.
Existing entryIts contents cannot be changed in place; a new key creates a new cache.Who created that entry and which newer keys a prefix can select.

For example, npm-${{ hashFiles('package-lock.json') }} makes the lockfile hash part of the lookup key.[1] That expression hashes the lockfile, not the restored archive. Our inference is that a matching lockfile-derived key is not an independent comparison of the cache's contents against approved bytes. A broad restore-keys prefix widens the candidate set; it does not validate the chosen files.

List writers by scope, not by workflow name

Runs can restore caches from their current branch and the default branch. Pull-request runs can also restore base-branch caches, including the base of a fork pull request.[1] Multiple workflows in the same repository and branch can share caches, so a file named release.yml is not a separate cache trust boundary.[1] Include every write-capable workflow whose entries the consumer's keys and reachable scopes can select.

Scope restrictions still matter. Caches from sibling or child branches and different tag names are restricted. A cache created by pull_request is scoped to refs/pull/.../merge and can only be restored by reruns of that pull request, not by the base branch or another pull request.[1] Do not assume an ordinary fork PR can write into default-branch scope. Inspect the actual event, ref and overrides instead.

For default-branch scope, GitHub lists push, workflow_dispatch, repository_dispatch, delete, registry_package, page_build and schedule as write-eligible triggers by default. Other events resolving to the default branch, including pull_request_target, issue_comment and workflow_run, receive read-only access to that scope unless explicitly overridden.[1] This restriction does not apply to pull_request caches, which already have merge-ref scope.[1] A write-eligible event label alone does not establish trusted actors or inputs: review who can initiate it, what code it checks out, what data it processes and what reaches the saved path.

Explicit cache-mode: write or cache-mode: write-only on a low-trust event bypasses that read-only default. GitHub warns that untrusted execution before save can then produce cache content later used by a more privileged workflow.[1][2] This is a documented risk mechanism, not an attack observed in a repository for this article.

Set restore and save rights explicitly

cache-mode capabilities. The word write includes restore access; write-only does not.[1][2]
cache-modeRestoreSave
readAllowedNot allowed
writeAllowedAllowed
write-onlyNot allowedAllowed
noneNot allowedNot allowed

Set cache-mode at the workflow's top level or under jobs.<job_id>; a job-level value overrides the workflow default for that job.[2] It is not an actions/cache with: input. GitHub enforces these capabilities using scoped cache tokens.[2] Do not infer cache-save rights from permissions: contents: read, which is a different control.

Reusable workflows need an explicit caller limit. A cache-mode set on the calling job, or inherited explicitly from the caller workflow, caps what the called workflow can request.[1][3] If neither is explicit, the called workflow can request write even when the caller's trigger defaults to read.[1][3] To keep the call restore-only, set cache-mode: read on the calling job and check the called workflow's request.

An explicit read caller cannot call a workflow requesting write; GitHub reports a validation error and the run does not start.[3] read and write-only are non-overlapping capabilities, not adjacent steps on a permission ladder: a write-only caller cannot permit a called workflow requesting read either.[3] This cap limits operations; it does not authenticate files that a permitted restore returns.

The runner exposes effective mode in ACTIONS_CACHE_MODE, which actions/cache and @actions/cache honor. A mode-disallowed restore or save is skipped with informational logging while the step and run continue; a skipped restore is treated as a miss.[1] GitHub also describes attempted read-only saves warning without failing the job.[1] Therefore a green run is not evidence that a save occurred, and a miss does not always mean there was no stored entry. Inspect effective mode and operation logs rather than filling in either conclusion from job status.

Review one producer-to-consumer path

Use this proposed preflight before accepting an agent's cache change. It is a review method derived from the documented behavior, not a GitHub certification or an executed test.

  • 1. Start at the consumer. Identify its actual repository, registry and deployment rights, accessible secrets, and the first command that loads or executes restored files. Decide whether those files will be used while elevated authority is available.
  • 2. Inspect the paths. Resolve every cached path and glob, including credential or configuration files beneath it. Exclude secrets, access tokens and login credentials: applicable base-branch caches can be read through pull requests.[1] A narrower directory reduces what is exposed but does not sanitize its contents.
  • 3. Resolve selection. Read the primary key, compatible cache version and every restore-keys fallback. Trace current/default/base branch eligibility and merge-ref restrictions. Note where a prefix admits more entries than an exact key.[1]
  • 4. Enumerate producers. For each eligible writer, inspect effective workflow/job mode, event/ref, initiating actors, checked-out code and inputs processed before save. Include other workflows sharing the repository/branch cache scope.[1][2] Do not stop at the producer you intended to use.
  • 5. Check reusable calls. Resolve explicit workflow and calling-job limits as well as the called workflow's requested capability. Use an explicit read caller cap where only restore is needed; a trigger-derived read default is not that cap.[3]
  • 6. Choose the operations. Use read for restore-only work, write when both restore and save are justified, write-only when only saving is justified, or none for neither. Keep the dependency path able to operate without a usable cache; effective mode and logs must distinguish selection, skipped operations and actual saves.[1][2]
  • 7. Decide the privileged restore. If the reachable producers, saved paths and first privileged use are unresolved, decline that restore and regenerate from the release's approved inputs. If they are established, allow only the reviewed path and required capability. Regeneration addresses this cache boundary; dependency sourcing, install behavior and runner security still need their own review.

Three completed hypothetical decisions

All three cases below are hypothetical and were not run. Their workflow conditions and policy choices are stipulated for explanation, not findings about an existing team. The decisions apply the proposed preflight rather than report measured safety or performance.

Hypothetical producer, scope and consumer decisions; NOT RUN.
CaseAssumed boundaryCompleted decision
A. Maintained dependency cacheReviewed push producer on main; credential-free package cache; ordinary build consumer.Use cache-mode: write for the reviewed producer path; retain a cache-free install path.
B. Low-trust restore-only jobissue_comment job resolving to main; no privileged execution; reusable build call.Use read and an explicit read caller cap; reject a write-requesting callee.
C. Privileged release hitExact primary-key hit; publishing credentials; reachable writers unresolved.Decline the restore and regenerate from approved release inputs.

A. The hypothetical team reviews who can push to main, the producer's code and inputs, and a ~/.npm cache path that excludes credentials. It chooses cache-mode: write because this producer needs both restore and save. It removes broad fallback keys that admit unreviewed producer paths and keeps installation possible on a miss. The push label is not the reason for acceptance; the stipulated producer/path review is. No saved entry, successful run or speedup is claimed.

B. The hypothetical issue_comment job intentionally restores only. It sets cache-mode: read on its reusable-workflow calling job, and the called workflow also requests read; its cache step uses a restore-only operation such as actions/cache/restore.[1][3] Assume no primary-key match and two compatible entries in eligible main scope: npm-main-lock-A and a more recently created npm-main-lock-B. With restore-keys containing npm-main-, the documented rule selects B as the newest partial match.[1] The job therefore proceeds through its fallback dependency path, without treating this as an exact primary-key hit or saving a cache. The team accepts that fallback only for this stipulated nonprivileged consumer. The same fallback would need a separate review before a privileged use; read-only files are not necessarily trusted files.

C. The hypothetical release job gets an exact hit under a lockfile-derived key. Its proposed change review has not established all write-capable workflows in reachable scope, or whether any processes untrusted input before save. The decision is to decline restoration into this credentialed job and regenerate dependencies from approved release inputs. Even if the key is exact and the entry is immutable, neither establishes its creator or verifies its bytes.[1] Narrowing restore-keys alone would not resolve this case: the exact-hit producer path is still unknown.

Keep the decision tied to the reviewed path

A cache can reduce repeated dependency work without serving as build provenance. This resource evaluates cached inputs to CI; it does not validate an artifact attestation or authorize publishing an output. Read-only access blocks a cache write route, while producer review and the consumer's use of restored files answer a separate trust question.

The cited sources are current, unversioned github.com documentation observed on 14 October 2026, Asia/Seoul. Their publication/update dates and the feature's introduction date are unknown. The captures do not establish minimum supporting action/runner versions, account-specific enforcement or compatibility with a particular GitHub Enterprise Server release. Check the documentation for your exact platform and supported versions before applying these controls; source syntax is not proof of an installed configuration.

Revisit the decision when a workflow gains save capability, accepts new inputs, broadens fallback keys or cached paths, changes a reusable caller, or starts using restored files with greater authority. Ask the agent's proposed change to explain those differences. If it can only point to cache-hit=true or a green job, the evidence needed for a privileged restore is still missing.

Sources & scope

A proposed CI review process based on unversioned github.com documentation observed on 14 October 2026, Asia/Seoul. The documents' publication/update dates, feature introduction date, minimum supporting runner/action versions and exact GitHub Enterprise Server support are unknown. No repository configuration, cache archive or workflow execution was inspected. All worked cases are hypothetical and were not run; no speedup or security-test result is reported.

  1. GitHub Docs: Dependency caching reference ↗

    Observed 2026-10-13T17:44:04Z (14 October, Asia/Seoul). Supports key/version selection, scope restrictions, modes, low-trust defaults, unsigned-content and secret-exposure limits. Publication/update date unknown; documentation, not a repository test.

  2. GitHub Docs: Workflow syntax for GitHub Actions ↗

    Observed 2026-10-13T17:47:06Z (14 October, Asia/Seoul). Supports top-level and job-level cache-mode placement, scoped-token capabilities and override warnings. Publication/update date and minimum supporting versions unknown.

  3. GitHub Docs: Reuse workflows ↗

    Observed 2026-10-13T17:47:06Z (14 October, Asia/Seoul). Supports explicit caller caps, default-versus-explicit distinction and over-request validation. Publication/update date unknown; interoperability was not tested.

Publication history
  • — Initial prepared edition: cache producer, scope, selection and consumer review with hypothetical decisions. Edition timestamp does not establish actual publication.

Have a correction or a different perspective? Contact Palanthos.