A cache key is one opaque hash. When it changes, knowing that it
changed is nearly useless. You need to know which of its parts moved.
Most tools do not keep that information; they compute the hash, compare
it, and print miss. The afternoon you then spend bisecting your own
inputs is the real cost of the cache.
vx records the per-component input fingerprint alongside every cache
entry. That makes the question answerable from the terminal:
$ vx why app#build
app#build — run 019f5a02-…
this run 2026-07-13T05:39:20.590Z · success · executed · key f7ee661520…
previous 2026-07-13T05:37:29.550Z · success · key 8b2e9bb2e8…
verdict cache key changed: file packages/app/src/input.txt
what changed (1 component, 41 unchanged):
changed file packages/app/src/input.txt 3fe2a1b0… → 91c47d22…
what to do:
file an edit re-runs by design; a file the task does not read belongs out of cache.inputs.files
One component moved and it is named. Forty-one did not.
When the component that moved is upstream, a dependency's key, that
row only carries the change. vx why follows each dependency that
moved in the same run down to the task whose own inputs moved, and
prints the chain:
root cause:
app#build ← lib#build ← file packages/lib/src/index.ts
What it reads
vx why is read-only over the local cache.db. It evaluates no
project config (the workspace file, once, for the cache directory,
unless --cache-dir names it), re-hashes nothing, runs nothing. It
compares the task's latest recorded run with the one before it (or a
run you pin with --run) and diffs the stored components, one row per
kind: file (path and blob id), env (a declared variable), runtime
and ws-runtime (a declared command's output, project- or
workspace-rooted), forward (the argv forwarded after --), package
(the project's own package.json), workspace (the fingerprint),
config (the evaluated task config), upstream (a dependency's input
key, by task id), plugin (a plugin's material, by name) and format
(the key format, which moves only with a vx upgrade). When
@vzn/vx-lockfile is declared, a dependency bump shows up as
plugin @vzn/vx-lockfile/pnpm for exactly the projects it reaches.
Because it only reads the database, it works after the fact and on
another machine: a CI job that copied .vx/cache out can be asked why
it rebuilt, tomorrow, from a laptop.
Update (2026-10-07): by default the entries vx why compares live in
the shared store, ~/.vx/<id>/cache, not in .vx/cache. A CI job that
names a cacheDir keeps both there, and that directory is the one to
copy.
Eleven endings for an unchanged key
The interesting cases are the ones where the key did not change, and
vx why distinguishes them rather than calling all eleven a re-run. The
verdict line is one of these fifteen sentences, quoted from the code:
| vx says | What happened |
|---|---|
| cache key changed: file packages/app/src/input.txt | the key moved; up to three components are named, then a count, and listed below |
| cache key changed between the previous run and this one (inputs differ) | the key moved but neither entry kept its components (pruned, or a failed run saved none) |
| cache key unchanged — this run was served from cache, nothing re-ran | it was a cache hit |
| cache key unchanged — the previous run on this key failed and saved nothing, so there was nothing to hit | a failure saves no entry |
| cache key unchanged — re-executed because this run did not read the cache (--force, or a --cache without read) | the run's policy read no cache |
| cache key unchanged — neither run saved it: each ran beside a failed task (…), and a task run past a failed dependency (--continue) is never cached | both runs went past a failure under --continue
|
| cache key unchanged — the previous run on this key ran beside a failed task (…) and was not saved: a task run past a failed dependency (--continue) is never cached, so there was nothing to hit | the previous run went past a failure under --continue
|
| cache key unchanged — no entry for this key was in the cache when it ran (pruned or evicted), so it executed and saved one | the entry was gone |
| cache key unchanged — the previous run on this key executed but no entry for it is in the cache (its save failed, or it was pruned since), so there was nothing to hit | the previous run's save failed, or a prune took the entry |
| cache key unchanged — the previous run on this key did not write the cache (--no-cache, or a --cache without write), so there was nothing to hit | the previous run's policy wrote no cache |
| cache key unchanged — re-executed on the same key though this run read the cache; vx cannot name the cause | none of the above; vx cannot name the cause |
| cache key unchanged — re-executed on the same key (--no-cache / --force, or unrelated) | no invocation recorded the run's cache policy |
| cache key unchanged — this run recorded no cache outcome, so whether it re-ran is unknown | the run recorded no outcome for this task; vx says so, not guesses |
this task declares no cache block — it runs on every invocation; its key is folded by dependents only |
not a cache decision at all |
| this task recorded no cache key (skipped, or a persistent task) — nothing to compare | there is no key to compare |
The "re-executed on the same key" row is the one to read
twice: vx could not name the cause. An undeclared input does not end
up there. A file, env var or tool version the key cannot see changes
the output but not the key, so the task hits and replays stale bytes.
The way to make that impossible is the sandbox,
which turns the input declaration into a boundary the task cannot
cross.
The same answer for machines
--format json emits one object: { taskId, runId, why, diff, roots }. The
@vzn/vx-mcp plugin exposes the same query as a tool a coding agent
can call (whyDidThisRerun), so "why is CI rebuilding everything" is a
question an agent can answer without reading the source of the runner.
A cache is a claim that the work has been done before. vx why is how
the claim is audited. The guide is
Configure › Why did it re-run?.
Originally published on the vx blog. vx is an MIT task runner and build cache for JS monorepos: github.com/vznjs/vx.
Written with AI assistance.
Top comments (0)