Restore keys are safe when they only speed up a download cache and your install still follows the lockfile. They become risky when a loose prefix restores an old or tainted cache and your build trusts the result. Keep the exact key tied to the lockfile hash and the fallback prefix narrow.
How matching works
GitHub's caching documentation describes the order. The action first looks for an exact match on the key. If there is none, it checks each restore key in sequence and looks for caches whose key starts with that string. If several caches match one restore key, it returns the most recently created. An exact match counts as a hit. A prefix match counts as a miss that still restores something, and a new cache is saved when the job succeeds. The docs also say a cache cannot be modified after it is created.
Take this example from the docs.
restore-keys: |
npm-feature-${{ hashFiles('package-lock.json') }}
npm-feature-
npm-
The last line, a bare prefix, matches any cache whose key begins with npm-. That is a wide net.
Where the risk comes from
Speed is the point of a fallback, and a stale cache is usually harmless because the installer then adds or updates what the lockfile requires. The risks are narrower, and worth naming:
- Wrong content restored. A prefix shared by two projects or two branches can restore a cache built for something else. The build may succeed with extra, unneeded packages sitting in it.
- Tainted content kept alive. If a cache was written by a job that ran untrusted code, a prefix match can carry that content into a trusted build. See can fork pull requests poison your GitHub Actions cache.
- Old versions masking a change. If the build trusts whatever is on disk, a stale package can stay in use after a security fix is merged.
- Recency hides age. Most recently created is not the same as most recently verified.
Safer key design
| Practice | Why it helps |
|---|---|
| Exact key includes the lockfile hash | A hit means the dependency set is the one you approved |
| Prefix includes operating system and tool name | Stops a restore from an unrelated cache |
| No bare one word prefix | Avoids the wide net shown above |
| Cache the package download cache | Content is addressed by version and checked at install time |
| Strict install after restore | The lockfile, not the cache, decides what is used |
For npm, the strict install command removes an existing node_modules before it installs and exits with an error if the lockfile and package.json disagree, according to the npm ci documentation. That is why caching the download cache and running a strict install is safer than caching and trusting a full node_modules tree. The tools are compared in frozen lockfile flags.
Who may write the cache
The safest cache is one only trusted jobs can write. Let pull request jobs restore, and let jobs on the protected default branch save. That limits who can plant content a later build restores. If you suspect a cache was tainted, clear it deliberately using purge CI caches after a compromise.
Where Safeguard fits
A restored cache is a set of packages entering your build. Safeguard's SBOM and dependency scanning records what each build used, so a stale or unexpected package shows up in the inventory, and a gate can block a build that fails your rules.
Quick check
Should I remove restore keys altogether?
Not necessarily. Keep one narrow fallback, such as the operating system and tool prefix, and make sure the install follows the lockfile.
Does a strict install make a stale cache harmless?
It removes most of the risk for the installed tree, because the lockfile decides the versions. It does not replace limiting who can write the cache.
Sources
Last reviewed: 19 October 2026.