9. Caching and cache trust
The cache is cook's center of gravity. Every cacheable unit (a step with at least one declared output) has exactly one content-addressed key, computed from…
The cache is cook’s center of gravity. Every cacheable unit (a step with at least one declared output) has exactly one content-addressed key, computed from what you declared: the input contents, the command text or Lua chunk, the output paths, any consulted environment values, and any sealed probe values. The local cache and the shared, cross-machine store are addressed by that same key, so a teammate or a CI runner reuses your artifact if and only if it recomputes the same key.
The engine folds in only what the author declared. It infers no machine identity, toolchain, or locale of its own; that keeps keys portable by default, and means correctness rests on you declaring your real inputs. Under-declaring is the failure mode, so cook gives you tools to see the key:
cook why [recipe]: read-only. Prints each unit’s key and every determinant behind it, with hit/miss status. On a shared miss it diffs against what the cached artifact was built from, so a miss is always attributable.cook cache verify: re-runs cached steps and reports any byte divergence under a matching key. A diagnostic for catching undeclared determinants (run it in CI on a different host), explicitly not a trust gate.
Per-step dispositions control keying and sharing. seal (the verb from
Using probes) folds declared determinants into the key, and a
trailing keyword on a cook step tunes how the artifact is stored and shared:
recipe misc cook "build/scratch.txt" { ./gen > $<out> } local # cached locally, never shared cook "build/pinned.txt" { ./gen > $<out> } pinned # fetch-only; a cold miss is an error cook "build/blurb.txt" { llm gen > $<out> } nondet # non-reproducible; reuse the recordingSealing composes with a top-level files declaration to solve a common bind: a recipe
whose gather line is already an iteration driver (a probe fan-out) has
no place to declare the other files its command reads. Name them as a
files declaration and seal it: every unit’s key now carries each file’s hash, and
cook why attributes a miss to the exact file that changed:
files sources "packages/*/src/*.ts" "packages/*/tsconfig.json"
recipe typecheck gather packages # the fan-out driver occupies this slot seal sources # so the source files ride the key instead cook "build/$<in.name>.stamp" { tsc -p $<in.dir> --noEmit && echo ok > $<out> }Unannotated steps publish after a run and fetch by key before running. Restored
bytes are re-fingerprinted before use, so a corrupted or tampered store entry is
treated as a miss and rebuilt. --no-publish (or COOK_NO_PUBLISH=1) makes a
client read-only: it still fetches, never uploads.
The shared store itself is a content-addressed directory: point [cache] cache_dir in .cook/cloud.toml at a path your team can reach and there is no
server to run. See Sharing a cache across a team for the
setup, the toolchain-sealing step it depends on, and the operational edges.
Keeping the store in budget
The local store grows without bound. cook cache du reports what is in it and
cook cache gc evicts on demand; two more keys in [cache] make it account for
itself at the end of every build:
[cache]max_size = "20GB" # unset (the default) means no budget and no warningauto_gc = false # the default: advisory only. true sweeps to 80% of the budgetWith max_size set and auto_gc left alone, a run that published something and
left the store over budget says so on stderr:
cook: warning: cache store is 23.1 GB, over the 20.0 GB budget (116%)cook: run `cook cache gc --max-size 20GB` to reclaim 3.1 GBcook: largest: /Cookfile::duckdb 11.0 GB (48%)The remediation command echoes your literal verbatim, so you can paste it. The
largest: line names the recipe namespace accounting for the most bytes, with
its share of the store, so you know what is filling it up; objects cook can no
longer attribute to a recipe count under (unattributed).
Opt in with auto_gc = true and the same run sweeps instead of warning:
cook: cache store was 23.1 GB, over the 20.0 GB budget — swept 4.2 GB (312 objects), now 18.9 GBThe sweep evicts least-recently-used artifacts down to 80% of the budget,
not to the budget exactly. Sweeping to exactly max_size would leave the store
one artifact from over-budget, so the next publishing run would sweep again, and
the one after that. The low-water mark buys several quiet runs between sweeps.
It gets as close to that mark as it can without evicting probe values or input
manifests, which the size sweep never touches: they are small and cheap to keep,
and losing them costs more than it reclaims. A store whose bulk is those kinds
can therefore report a sweep and still sit above the budget. Read the now figure
in the sweep line against your max_size; if the gap does not close across
several runs, cook cache du will show you what the sweep is declining to
evict.
--no-auto-gc (or COOK_NO_AUTO_GC=1) suppresses the deletion for one run and
nothing else: the warning still prints, the store is just not touched. That is
the CI contract: a job stays reproducible while its operator still learns the
store needs a sweep.
Three things the check will not do:
- Fail your build. An unreadable config, a typo’d
max_size, an unwalkable store, a sweep that errors: each degrades to acook: warning:line. A typo warns rather than quietly meaning “no budget”. - Cost a settled build anything. It runs only after a run that published
outputs, and that test comes before the config load and the store walk. An
incremental build with nothing to do never looks at the store, so a store can
sit over budget quietly while you are not publishing. There is no stamp file
and no rate limiting either: every over-budget publishing run reports, and
cook cache duasks on demand regardless. - Touch a server-managed cloud cache. The budget covers the local store only; a cloud backend runs its own retention.
One caveat if [cache] cache_dir points at a mount you share with your team.
With auto_gc = true there, your machine sweeps a store holding your
teammates’ objects, and several machines can sweep it concurrently.
Touch-on-read helps (the access time lives on the shared file, so recency
genuinely is shared) but the concurrent sweep is a real race. That is why
auto_gc is opt-in rather than on: enabling it on a shared store is opting into
that. On your own store the stakes are low, because a wrong eviction costs a
rebuild and not data.