Build the key from the runner operating system and a hash of your lockfile, so a dependency change creates a new cache. Add restore-keys only if you understand that a partial match restores the most recent cache with that prefix, which may have been built from different inputs.
How matching works
GitHub's caching reference describes the sequence. The action first looks for an exact match on the key. If none exists, it looks for partial matches of the key. If there is still no match and you provided restore-keys, it checks those in order. An exact match is a cache hit. On a miss, a new cache is saved when the job succeeds.
A cache cannot be changed once created. To change contents, you create a new cache under a new key. Keys can be up to 512 characters.
The search runs in the current branch first, and then, if nothing is found, on the default branch, subject to scope restrictions. For a pull request, caches from the base branch are reachable too, but not caches from sibling or child branches.
A key that follows the lockfile
GitHub's example builds a key from the runner OS and the SHA-256 of package-lock.json:
key: ${{ runner.os }}-build-cache-node-modules-${{ hashFiles('**/package-lock.json') }}
When the lockfile changes, the hash changes, and a new cache is created automatically. The docs recommend this expression pattern. Use the same idea for other ecosystems, hashing the file that decides your resolved versions: go.sum, Cargo.lock, poetry.lock, Gemfile.lock and so on.
If you use the package manager setup actions that handle caching, the docs list them: setup-node, setup-python, setup-java, setup-ruby, setup-go and setup-dotnet. They handle the key for you.
What restore-keys do to determinism
Restore keys list prefixes from most to least specific. If one matches, the most recently created cache with that prefix is restored. The example in the docs lists three prefixes, ending in a broad one.
That helps speed. It also means your build can start from a cache made from a different lockfile. For a package manager download cache, this is usually fine, because the package manager still resolves from the lockfile and fills any gaps. For a cache of build outputs, it can carry stale results into the new build.
| What you cache | Restore keys fine? |
|---|---|
| Package manager download cache | Usually, if the tool verifies against the lockfile |
| Compiled output or intermediate files | Only if the build tool re-checks inputs |
| Anything you will sign or ship | Prefer exact key only, or no cache |
For a release build, think hard about whether a cache belongs at all. A clean build from the lockfile is slower but simpler to reason about.
Handle cache content as untrusted
The docs advise not storing access tokens or credentials in cached files. They also note that anyone with read access can open a pull request and see cache contents, and forks can raise pull requests that access caches on the base branch. For the rules on who can write to default branch caches, see what cache-mode does.
Steps
- Choose the lockfile that fixes your versions.
- Build the key from OS and that file's hash.
- Add one restore key only if it is safe for what you cache.
- For releases, use an exact key, or no cache.
- Log the cache hit output, so you know which build was cached.
Try it yourself
Click through the demo and look at how resolved dependency versions are listed, the same information your lockfile hash stands for.
Click through it at your own pace, or open it full screen.
Quick check
Why not use the branch name in the key?
A branch name does not change when dependencies change. A lockfile hash does, and creates a fresh cache.
Can I edit a cache entry?
No. You create a new cache with a new key.
Safeguard's SBOM and dependency scanning is generally available and reads lockfiles. The free scan at gold.safeguard.sh/scan shows what yours contain.
Sources
Last reviewed: 19 October 2026.