The problem
GitHub stores cache entries for actions/cache, setup-node,
setup-python and similar actions, but there is no button, API call or gh
command that returns the archive. The cache service hands out download URLs
only to workflow runs. Debugging a cache that is restored but not used, or a
cache that is unexpectedly large, needs the contents.
The demand is documented:
- cli/cli#9125, "Add a way to
download github actions caches", has 113 reactions and has been open since
May 2024. A gh maintainer forwarded it to the Actions team in July 2025.
The workarounds in the thread are workflow snippets that restore the cache
with
actions/cacheand upload it as an artifact. They need the originalpathlist, because it determines the cache version. - actions/cache#105, "Ability to download cache and inspect contents", has 34 reactions. It was closed by the stale bot in 2023 after a maintainer said downloading was not planned.
gh cache list,gh cache deleteand actions/gh-actions-cache list and delete entries but cannot download them.
How it works
gh-cache-download has two
parts. The first is a gh extension, a single Python file without
dependencies. gh cache-download get KEY looks the key up with
GET /repos/{owner}/{repo}/actions/caches (exact match), dispatches a small
helper workflow on the branch that owns the cache, waits for the run and
downloads the result. gh cache-download init writes that helper workflow,
which has to be committed and pushed. It only runs when dispatched and has
read permissions only.
The second part is a composite action that the helper workflow uses. Inside
the run it calls the cache service endpoint GetCacheEntryDownloadURL with
the key and version, downloads the archive and uploads it as an artifact.
Because it fetches by key and version, it does not need to know which paths
were cached.
actions/cache stores paths relative to the workspace, so anything cached
outside it, such as ~/.npm, appears as ../../../.npm/... in the archive.
gh cache-download extract (or get -x) never writes outside the target
directory: each .. becomes a __parent__ directory, absolute paths lose
their leading /, and members that would escape, such as symlinks to
absolute paths, are skipped.
This is a real run against a cache saved by the repository's own CI from
e2e-data in the workspace and ~/.e2e-home outside it:
$ gh cache list -R Jwrede/gh-cache-download
9507487879 e2e-40288214454-1 459 B 2026-10-26T07:50:08Z 2026-10-26T07:50:10Z
$ gh cache-download get e2e-40288214454-1 -R Jwrede/gh-cache-download -x
Cache 9507487879: refs/heads/main, 459 bytes, version b107c4986568
Run 40288300859: https://github.com/Jwrede/gh-cache-download/actions/runs/40288300859
Archive: cache-9507487879/cache.tzst (459 bytes)
Extracted 6 entries to cache-9507487879/files
$ find cache-9507487879/files -type f -o -type l
cache-9507487879/files/e2e-data/link
cache-9507487879/files/e2e-data/sub/a.txt
cache-9507487879/files/__parent__/__parent__/__parent__/.e2e-home/b.txt
The whole get took 14 seconds.
Testing
The Python unit tests cover the key lookup (exact match only, prefix
matches listed when a key is missing, --ref required when a key exists on
several branches) and the refusal of pull request refs. Extraction is tested on
plain tar, gzip and zstd archives that contain a symlink, a hard link, a
path outside the workspace and a symlink to /etc/passwd, which must be
skipped. The get command runs against a fake gh that records the calls,
including a failed dispatch and a failed run. Node tests cover the action's
version selection and archive type detection.
CI runs the unit tests on Python 3.10 and 3.14. An end-to-end job creates
files inside and outside the workspace, saves them with
actions/cache/save, downloads the entry with the action, extracts it with
the CLI and compares the contents and the archive size.
Limitations
- A run can only read caches of its own ref and of the default branch, so the helper workflow must exist on the branch that owns the cache.
- Caches created by pull request runs (
refs/pull/N/merge) cannot be fetched with a manual dispatch. For those, the action has to be added to the pull request's workflow. GetCacheEntryDownloadURLis the internal API thatactions/cachev4+ uses. GitHub does not document it as a public API, so it may change. The tool is tested on github.com only, with archives from Linux runners.
The code and installation instructions are at github.com/Jwrede/gh-cache-download.