kache 1.2.2

Compiler cache for Rust, C/C++ and CUDA, with S3, GCS, shared-filesystem and OCI remotes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
---
title: Command reference
description: Every top-level Kache command, its purpose, and common flags
---

Run `kache <command> --help` for the exact syntax in your installed version.

Human output uses section headings, aligned values and status colors. Colors are disabled when stdout is redirected, `NO_COLOR` is nonempty, or `TERM=dumb`. Long summary notes wrap to the terminal width; paths remain complete.

`--json` emits one versioned JSON document for `init`, `list`, `stats`, `report`, `clean`, `gc`, `doctor`, `explain`, `diff`, `targets`, and `daemon status`. It works before or after the command. Use `init --json --check` to preview setup, or `init --json --yes` to apply it without prompts. `init --shims` has no JSON form. `doctor --json` only reports; combined with `--fix`, `--purge-sccache`, `--verify`, `--checksums` or `--repair` it exits with an error.

`clean --json` and `clean --units --json` preview target removal unless `--yes` is also present. A target `clean --json --yes` could not remove is listed under `failed` with its `reason`; the document then has `success: false` and an `error`, and the command exits 1. The cache forms preview with `--dry-run`: the document has `preview`, `entries`, `bytes` and `message`. `clean --cache --json` and `clean --crate <name> --json` are refused even with `--yes`. `clean --cache --stale <age> --json` and `clean --cache --stale-schema --json` evict at once and print the `gc` fields.

Every document includes `schema_version`, `command`, and `success`. Successful commands keep their existing fields and may include `next`, an array of suggested commands with `argv` and `why`. Failures include `error.code`, `error.message`, and `error.causes`, and exit nonzero: 2 for argument errors, 1 for command failures. Progress and warnings go to stderr. `success` means the command completed; diagnostic findings remain in the command's fields, such as `issues` for doctor.

The envelope is documented in [`cli.schema.json`](https://github.com/kunobi-ninja/kache/blob/main/docs/commands/cli.schema.json). Byte counts and durations stay numeric rather than formatted strings. `stats --json` includes count and compile-time hit rates, time saved, wrapper timings, daemon identity, and storage diagnostics. `stats --full --json` and `report --json` add the envelope fields to the full report. Explicit `--format json` keeps the standalone report schema. With `--json --output`, stdout contains a file receipt. `--json --help` describes the whole visible command tree and its flags, whichever command it follows; hidden commands and flags are left out. `--json --version` reports the version.

| Command | Purpose |
| --- | --- |
| `init` | Configure Cargo, optional C/C++ shims, test pacing, and the daemon service |
| `doctor` | Diagnose setup and store integrity |
| `config` | Open the configuration editor |
| `stats` | Hit rate, time saved and cache size; `--full` for the full build report. Alias: `status` |
| `clean` | Free disk space: remove old target directories, or empty the cache |
| `monitor` | Open the live dashboard |
| `explain` | Explain why a crate, or the latest build, missed the cache |
| `list` | Inspect local entries |
| `sync` | Pull from and push to a remote |
| `login` | Log in to the planner with a Kunobi account |
| `logout` | Log out of the planner and revoke the stored session |
| `daemon` | Inspect and manage the background daemon |
| `completions` | Generate shell completion scripts |

These are for CI, scripts and integrations, and are not listed by `kache --help`:

| Command | Purpose |
| --- | --- |
| `cargo` | Run Cargo with duplicate Cargo-home rustflags collapsed |
| `save-manifest` | Publish build metadata for later prefetch |
| `telemetry` | Write cache counters and this machine's shared-cache state as OTLP JSON for Kartero (`write`), or send build timeline records to the planner service (`push`) |
| `test-runner` | Run a test binary as Cargo's target runner, paced by the scheduler |
| `report` | Same report as `kache stats --full`, kept for scripts |
| `gc`, `purge`, `targets` | What `kache clean` does now (`targets` lists what `kache clean --dry-run` shows), kept for scripts |
| `targets share` | Find target files that duplicate stored blobs; `--apply` replaces them with clones. See [Sharing target files](#sharing-target-files) |
| `install-shims` | What `kache init --shims` does, kept for scripts |
| `diff` | Misses and the share of unexplained misses in the latest build against the one before; exits 1 when either grew. Kept for scripts |
| `why-miss` | The earlier name of `kache explain` |

## `kache cargo`

```bash
kache cargo -- build
kache cargo -- check --workspace
```

Pass Cargo arguments after `--`. Kache runs Cargo and holds the lock that keeps `kache clean` and the daemon from deleting target directories until Cargo exits. For `build` and `check` without `-C`, `-Z` or `--config`, it also:

- passes `build.rustflags` once when an ancestor `.cargo` directory links to `$CARGO_HOME`, which would make Cargo read the same flags twice. Any `RUSTFLAGS`-style variable turns this off.
- on Cargo 1.91 or newer, sets `build.build-dir` to `{workspace-root}/target` unless `CARGO_BUILD_BUILD_DIR` or a `build.build-dir` setting exists. Each worktree then keeps its own intermediate files while final artifacts go to the shared target directory.

Other commands, such as `test`, run without these changes.

If `kache clean` or the daemon's target cleanup holds that lock when Cargo starts, Kache prints `kache: waiting for target cleanup (pid 4242) to finish` on stderr and starts Cargo once the cleanup releases the lock. Cargo's `-q` or `--quiet` hides the line. A test run through `kache test-runner` waits the same way.

## `kache list`

```bash
kache list
kache list serde
kache list --sort size --no-pager
```

Sort values are `name`, `size`, `hits`, and `age`.

Interactive output uses `KACHE_PAGER`, then `PAGER`, then a platform default. `--no-pager` writes directly.

## `kache clean`

```bash
kache clean --dry-run
kache clean
kache clean --yes
kache clean --stale 7d --yes
kache clean --orphans --yes
kache clean ~/src --dry-run
```

`clean` frees disk space. Without a directory it works from the target directories that builds through Kache used. It selects targets whose worktree or workspace has been deleted, and targets no build has used for 14 days. `--stale` changes that window and takes the same forms as `kache stats --since`; a value under an hour counts as one hour. `--orphans` selects only deleted worktrees, however recently they were built. The daemon removes those targets on its own a day after their last build, so `--orphans` is for removing them sooner, for example from a hook that runs after `git worktree remove`. A workspace counts as deleted only while the directory that held it still exists, so an unmounted volume does not.

`--dry-run` lists what the clean would remove and every target it keeps: what deleting each frees, what it holds on disk, how long since a build used it, and what the daemon's next quiet pass would do to it. It ends with the cache's size and the command that removes the selection. The freeable estimate leaves out blocks a target shares with the store or with other hardlinks, so it is what the clean gives back. `--dry-run` also lists the default targets of sibling Git worktrees, marked "found through Git", but `kache clean` skips them because Kache has not seen a build use them.

Without `--dry-run` or `--yes`, a selector opens in a terminal. Without a terminal the command removes nothing, exits 1 and prints the same command with `--yes`. `--cache`, `--crate` and `--units` ask the same way before removing. `--json` describes the selection and removes nothing unless `--yes` is also set.

Given a directory, `clean` looks under it for `target/` directories that sit next to a `Cargo.toml`, including worktrees under `.worktrees/` and `.claude/`. Other hidden directories and `node_modules` are skipped. Sizes cover every profile, custom profiles and `<triple>/<profile>` directories included. A directory selects every target under it, so it cannot be combined with `--stale` or `--orphans`.

Removal rechecks each directory's identity and skips one that changed since the scan. For tracked targets it also skips paths that are missing, moved or no longer a derived target directory, and targets of the current workspace. A directory you name gets no such filter, so `kache clean . --yes` at a workspace root removes that workspace's `target/`. A target is skipped in every mode while a running build holds its `.cargo-lock`. While any Cargo command started through Kache (`kache cargo` or the `cargo` shim) or any `kache test-runner` test is running, the clean skips every target. A target that cannot be removed is reported and makes the command exit 1.

The daemon removes tracked targets on its own, on a quiet machine at most once an hour: a target whose workspace was deleted, once no build has used it for a day (`auto_clean_orphaned_targets`, on by default); idle ones when `auto_clean_idle_targets_days` is set; and, when `auto_recover_targets` is on, idle ones while free disk space is below the `auto_recover_min_free_bytes` floor. See [Target directory cleanup](/docs/getting-started/configuration#target-directory-cleanup).

### Obsolete units in live targets

```bash
KACHE_TARGET_LIVENESS=1 kache cargo check
kache clean --units target --dry-run
kache clean --units target --yes
```

`--units` (also `--artifacts`) removes known obsolete build units from intact tracked targets. A path selects the target directory or its workspace. Without a path it selects all live tracked targets. It cannot be combined with `--cache`, `--crate`, `--stale`, `--orphans` or `--stale-schema`.

A successful supported Cargo `build` or `check` command supplies the receipt, including its Fresh artifacts. Cleanup preserves the recorded command's live units, other profiles and unknown units. Missing or changed evidence keeps everything. The preview explains why a target cannot be cleaned and how to capture a receipt. It asks before removal; `--yes` confirms, `--json` previews unless confirmed, and `--dry-run` always previews. See [Recorded-command cleanup](/docs/getting-started/configuration#recorded-command-cleanup) for the scope and safety checks.

### The cache

```bash
kache clean --cache --dry-run
kache clean --cache
```

`--cache` empties the main store instead of removing target directories; volume shards keep their entries. It asks first, and without a terminal it removes nothing unless `--yes` is set. It also deletes the index files kept after index corruption, and says how many before it asks. It removes hermetic build-script runs that no target directory links to. Tracked target directories stay tracked.

You rarely need `--cache`: the daemon evicts entries on its own to keep the cache under `cache.local_max_size`, starting with stale, rarely hit entries that free the most space. For scripts, `--cache --stale 30d` evicts entries older than that and `--cache --stale-schema` removes entries created with old or unrecorded key schemas, both without asking; `--crate serde` removes one crate's entries after asking.

Eviction affects only the local store. It also sweeps each `[cache.volumes]` shard against that shard's own budget; see [Configuration: Volume shards](/docs/getting-started/configuration#volume-shards). `--stale-schema` sweeps only the main store.

Eviction reports store bytes removed separately from bytes reclaimed on disk. It keeps an entry when its last store blob is still cloned into a build output, because unlinking that blob would free no disk and destroy a usable hit. This can leave the registered store size above `cache.local_max_size`; remove stale targets to reclaim the blocks. A size sweep leaves those held bytes out of its budget instead of evicting other entries to make up for them.

Set `cache.gc_evict_shared = true` or `KACHE_GC_EVICT_SHARED=1` to restore the older namespace-first policy. That lowers the registered store size but does not reclaim blocks retained by target directories, and the discarded entry becomes a future miss. See [Configuration: GC and shared build outputs](/docs/getting-started/configuration#gc-and-shared-build-outputs).

Eviction flushes any entries still pending durability before it judges them (see [Architecture](/docs/how-it-works/architecture)).

`kache gc` (evict by the size and age limits now, as the daemon does), `kache purge` and `kache targets` still work as they did, for scripts written before `clean` took them over. They are hidden from `--help`.

### Sharing target files

```bash
kache targets share
kache targets share --apply
kache targets share --apply target/
```

`targets share` reads the tracked target directories, or the directories named after it, for compiler outputs of at least 64 KiB (`.rlib`, `.rmeta`, `.so`, `.dylib`, `.a`, `.o`, `.dll`, `.lib`, `.exe`, `.pdb`) that are full copies of blobs in the store. A file whose length matches no stored blob is finished after a `stat`. A file that differs from the blob in its first or last 4 KiB is finished after those two reads. Only a file that survives both is hashed. Kache stores that hash with the file's path, size, modification and change times and inode, so a later run reads nothing from an unchanged file.

`--apply` replaces a proven match with a clone of the stored blob. Cloning needs a filesystem that supports file clones (reflinks). On other filesystems every match stays in place and is counted as failed. The replacement keeps the file's mode and modification time. The pass takes the profile's `.cargo-lock` and skips a target a running build holds. It does not hardlink, it does not copy a file onto another volume, and it does not enter `incremental/` or `.fingerprint/`. Without `--apply` it only reports.

With `cache.auto_share_target_files` on (the default), the daemon runs one short slice of the same pass, with `--apply`, while the machine is idle. The slice stops when a compile starts. A check that already repaired the index or swept targets does not also run the slice.

## `kache init`

```bash
kache init
kache init --check
kache init --no-service
kache init --no-shell
kache init --yes
```

`--check` (also `--dry-run` or `-n`) lists the changes init would make, including the `build.rustdoc` edit, without writing. It ends with the `kache init` command and flags that apply them, or says there is nothing to change. With `--json`, each entry in `steps` says what init does to one component: `configure`, `update`, `keep`, or why it cannot (`unsupported`, `unavailable`, `manual`). `free_space_recovery` is `ask` when an interactive run would ask about removing idle targets. A `backup` step names each existing file init would copy before editing it. After a real run, `steps` says what was done: a declined step is `skip`, and one that did not succeed is `failed`. `--no-service` skips login-service installation. `--no-shell` leaves shell startup files alone: no shim directory on `PATH` (C/C++ compilers, `cargo` and `rustdoc`), no `build.rustdoc` edit and no test pacing. Cargo's native dependencies are still configured through `[env]`. When the service cannot be installed, such as on a Linux host without a systemd user session or a Windows account that Task Scheduler refuses, init reports why and starts the daemon directly.

Init shows a setup plan and asks once before applying it. Replacing another Cargo wrapper requires a separate confirmation. `--yes` accepts both. If stdin has no answers, init exits 1 before changing files or services and prints the same command with `--yes`. Init backs up a file before it edits it, but not a file the same run created. Besides `rustc-wrapper`, init adds `HOST_CC` / `HOST_CXX` / `CC_KNOWN_WRAPPER_CUSTOM` under Cargo `[env]` when those keys are absent. On Windows it adds only `CC_KNOWN_WRAPPER_CUSTOM`. It never sets `CC` or `CXX`.

On Unix, when `build.rustc-wrapper` is `kache` and `~/.local/lib/kache/shims` holds a `rustdoc` link, init sets `build.rustdoc` to that link and backs up the Cargo config first. Cargo then runs `cargo doc` through Kache. Documentation caching requires compatible unstable Cargo flags; Rust 1.99 currently passes through. See [Rust documentation](/docs/getting-started/rustdoc) for setup and compatibility. Init does not replace a `build.rustdoc` value you already set. `--no-shell` skips this edit. Windows has no compiler shims, so `cargo doc` is not cached there.

On Unix, init also configures test pacing: it exports `CARGO_TARGET_<HOST>_RUNNER="kache test-runner"` from the same shell startup files the C/C++ setup edits, so `cargo test` in every project waits for the scheduler's slots. A value you already export stays. `--no-shell` skips both edits. See [Test runner](/docs/getting-started/configuration#test-runner).

### Compiler shims

```bash
kache init --shims
kache init --shims --from-path
kache init --shims --force
```

Unix only. `kache init` offers to create them; `kache init --shims` creates only them. It makes `cargo`, `cc`, `c++`, `gcc`, `g++`, `clang`, `clang++`, and `rustdoc` symlinks to Kache. The `cargo` link runs the real Cargo (`KACHE_REAL_CARGO`, else the next `cargo` on `PATH`) and holds the lock that keeps `kache clean` and the daemon from deleting target directories until it exits. `kache init` calls this Cargo target protection.

The farm is `~/.local/lib/kache/shims` unless a directory follows `--shims`, as in `kache init --shims /usr/lib/kache`. It also links each versioned C and C++ compiler on `PATH` (`clang-19`, `clang++-19`, `gcc-13`), matched by name without running it; `--canonical-only` leaves those out. The hidden `kache install-shims` does the same, for older scripts. `--from-path` also wraps every other compiler name on `PATH`, such as target-prefixed drivers. Put the directory before the real compilers in `PATH`. If the directory then holds only Kache links, Kache also writes a `.kache-shims` marker. When Kache looks for the real compiler, it skips everything in a marked directory. A directory that also holds other files, such as a real compiler, is left unmarked.

The links point at a path that survives upgrades when one reaches the running binary: Homebrew's `opt` link, a Nix profile, mise's `latest` alias, or a `kache` on `PATH` outside version directories. Without `--force`, existing entries are kept, except in a directory Kache created, marked, or filled only with Kache links. There a rerun replaces Kache links whose target is missing or not executable, and moves Kache links into a version directory to the upgrade-safe path. Other links are kept. See [C and C++](/docs/getting-started/c-cpp#upgrades).

## `kache doctor`

```bash
kache doctor
kache doctor --fix
kache doctor --verify
kache doctor --checksums
kache doctor --repair
```

- `--fix` migrates from sccache: it stops a running sccache server, replaces `sccache` with `kache` in `$CARGO_HOME/config.toml`, and prints the lines to change in your shell startup files. It does not set up other missing pieces such as `rustc-wrapper` or the shims; run `kache init` for those.
- `--purge-sccache` also deletes the sccache cache directory (`~/Library/Caches/Mozilla.sccache` on macOS, `~/.cache/sccache` on Linux) and runs `cargo uninstall sccache` when sccache was installed with Cargo. It requires `--fix`.
- `--verify` checks entries, blobs, and metadata. It exits 1 while corrupted entries or index mismatches remain, so a scheduled job can gate on it. Orphan blobs do not count.
- `--checksums` also hashes blob contents and implies `--verify`.
- `--repair` removes corrupted entries, reconciles the index with the entries on disk, reclaims orphan blobs, prunes stale C/C++ memos, input predictions and old file hashes, and compacts a large, sparse index when disk space allows. It implies `--verify`.

`--purge-sccache` and `--repair` delete data. Check what they will remove before running them.

Doctor reports issues and still exits 0; scripts read them from `kache doctor --json`. Only `--verify`, and the options that imply it, exit 1 for what they find.

Doctor also reports, for information, how many misses in the latest build differ from the same crates built in another checkout ("Other checkouts"); `kache explain` says what to set. It reports where the running binary is installed ("Install path"), whether the C/C++ shims are active, broken, not first on `PATH` or not installed, and whether the login service runs a binary that no longer exists, is not executable, or is a different file from the running Kache. Run `kache daemon install` to fix the last one.

With a remote configured, `kache doctor` also asks it for an object that is never written. A miss passes: the store answered and accepted the credentials. A failure names the likely cause when the store says, such as expired credentials, a secret key that does not match, a clock too far from the store's, the bucket's real region, or a bucket that does not exist. An S3 endpoint that uses plain `http://` to another machine fails its own check. For S3, a pass names where the credentials came from, and a selected AWS profile that cannot supply credentials fails the check with the profile and the cause.

## `kache sync`

```bash
kache sync
kache sync --pull
kache sync --push
kache sync --dry-run
kache sync --pull --workspace
kache sync --pull --all
```

Options:

| Flag | Effect |
| --- | --- |
| `--manifest-path <PATH>` | Cargo manifest whose workspace members filter what is pushed and, with `--workspace`, what is pulled. Defaults to `./Cargo.toml` when it exists; without one, pushes are not filtered and `--workspace` fails |
| `--pull` | Skip uploads |
| `--push` | Skip downloads |
| `--dry-run`, `-n` | Plan without transfer |
| `--all` | Ignore normal pull filtering |
| `--workspace` | Scope pull discovery to workspace members; conflicts with `--all` |
| `--allow-partial` | Exit successfully despite reported transfer/import failures |

See [Sync](/docs/remote-cache/sync) for filtering semantics.

## `kache save-manifest`

```bash
kache save-manifest
kache save-manifest --manifest-key x86_64-linux-release
kache save-manifest --namespace x86_64-unknown-linux-gnu/rustc-hash/release
```

Without `--manifest-key` or `KACHE_MANIFEST_KEY`, Kache publishes under the
host target triple. When `Cargo.lock` exists and `KACHE_PROFILE` or `PROFILE`
names the profile, it also publishes under an identity key,
`id/<lock-digest>/<host-triple>/<profile>`. `--manifest-key` or
`KACHE_MANIFEST_KEY` writes a single key. A namespace from `--namespace` or
`KACHE_NAMESPACE`, plus `Cargo.lock`, also publishes content-addressed shards.

The manifest lists each cache key in the retained event log once, from every
build the log still holds. Build-script runs are left out: they stay in the
local store, so no other machine could fetch them.

With a remote configured, the daemon also publishes a build session's identity
manifest when the session goes idle or a newer one replaces it; daemon
shutdown publishes nothing.

When the remote has no manifest for the current lockfile, the planner tries
the manifests of up to three earlier committed revisions of `Cargo.lock`
before the host-triple key, so a dependency bump still prefetches the part of
the graph it did not change. This reads git history: a shallow clone needs
enough of it (`fetch-depth` on GitHub Actions), and without git the step is
skipped.

CI jobs that share a key, such as tests and lints on the same lockfile, target
and profile, add to one manifest rather than replacing each other. Entries from
the publisher's commit (`GITHUB_SHA` or `CI_COMMIT_SHA`) stay together and
entries from any other known commit are dropped, so a rerun of an older
commit's job also drops a newer commit's entries. When either commit is
unknown, a stored entry stays for an hour after it was last published. A merged
manifest keeps at most 50,000 entries, putting the publishing job's entries
first.

On S3 the update is a conditional write: a job that loses a race reads the
manifest again and retries, and after five tries it fails and leaves the
manifest as the other jobs wrote it. Stores without conditional writes,
including filesystem remotes, get the merged manifest written plainly, so two
jobs finishing at the same moment can still drop each other's entries there.
Shards merge the same way.

## `kache daemon`

```bash
kache daemon             # status
kache daemon status
kache daemon run
kache daemon start
kache daemon stop
kache daemon restart
kache daemon install
kache daemon uninstall
kache daemon log
```

See [Daemon lifecycle](/docs/daemon/lifecycle).

## `kache monitor`

```bash
kache monitor
kache monitor --since 7d
```

`--since` takes the same forms as `kache stats`. Without it, the monitor loads the last hour. It needs a terminal; scripts use `kache stats --json`.

See [Monitor](/docs/monitor).

## `kache stats`

```bash
kache stats
kache stats --since 15m
kache stats --full
kache stats --last-build --root "$PWD"
```

`kache stats` (alias `status`) groups its summary into Builds, Lifetime, Storage and Services. `Cache` counts registered, deduplicated blob bytes. GC compares them with `Limit`, leaving out blobs that a `target/` directory still holds as a clone, because evicting those frees no disk; `On disk` shows that split, so `Cache` can stay above `Limit` with nothing left to evict. `Volume` shows the free and total space of the volume holding the cache, and whether [free-space recovery](/docs/getting-started/configuration#target-directory-cleanup) is on; JSON carries it as `volume` (`total_bytes`, `free_bytes`, `recovery_floor_bytes`, `below_floor`, `recovers_targets`), absent on Windows. `Dedup` shows logical entry bytes and the space saved by sharing blobs. `By time` weights hits by compile work, and `Miss time` shows the time spent on misses with its share of wrapper time. Index size and WAL size stay in the summary; rowid high-water marks appear under `--full` and in JSON. `--full` prints the full build report described below.

Below the window's rows, a section starting with `Since` shows totals since kache began counting: compile time avoided and the number of hits, bytes restored by reflink or hardlink against bytes copied, and store bytes GC removed, split into automatic runs (the daemon and the auto-GC worker) and `kache gc` runs you started. The event log rotates by size, so each rotation adds the events it drops to `savings.json` in the cache dir, and each GC run that frees bytes adds them there too. The totals are that file plus the events the log still holds, and volume shards add their own GC totals. The section is left out until something is counted. A crash between a rotation and its update of `savings.json` loses those events from the totals; nothing is ever counted twice. A corrupt `savings.json` is moved to `savings.json.corrupt` and counting starts over; one written by a newer kache is left alone. `kache stats --json` carries the same figures in a `lifetime` object (`since`, `hits`, `hit_compile_time_ms`, `zero_copy_bytes`, `copied_bytes`, `pruned_automatic_bytes`, `pruned_requested_bytes`).

`--since` accepts `s`, `m`, `h`, and `d` units (`90s`, `15m`, `2h`, `7d`). A bare number is hours. The default is `24h`. The counters and the `last ...` label both describe the requested window. A value that does not parse, or 0, is an argument error.

`--last-build` shows the detailed report for the latest recorded activity session. Add `--root` to choose a build tree; without it, the latest event across all roots chooses the session. It cannot be combined with `--since`. With `--json`, the stats envelope contains a `report` object using the build report schema below.

`--record` also appends one JSON line to `telemetry/sessions.jsonl` in the cache dir. The line holds the report's summary and timing breakdown, and the machine's load average, CPUs and index size at that moment. The root is stored as a hash. The runtime dir a CI job deletes on exit is where the report reads its events; this line is what survives on the machine. `KACHE_RECORD_SESSIONS=1` or `[cache] record_sessions = true` makes every report record the same way without the flag, and appends each GC run to `telemetry/gc-runs.jsonl`; it is off by default. With neither, the command writes nothing.

### The full report

```bash
kache stats --format text --since 24h
kache stats --format markdown --root "$PWD"
kache stats --full --last-build --root "$PWD" --format markdown
kache stats --format json --output report.json
kache stats --format trace --output trace.json
```

| Flag | Default | Values or purpose |
| --- | --- | --- |
| `--full` | off | The full report instead of the summary. `--format`, `--output`, `--top`, `--record`, and `--redact` imply it |
| `--format` | `text` | `json`, `trace` (also `perfetto`, `chrome-trace`), `markdown` (`md`), `github` (`gh`), `text`. Any other value is an argument error. `--json` gives `json`, and combining it with a format other than `json` or `text` is an error |
| `--since` | `24h` | Event window; same forms as `kache stats` (`15m`, `2h`, `7d`, bare hours) |
| `--last-build` | off | Latest recorded activity session; conflicts with `--since` |
| `--root` | all roots | Only events from that build tree and the trees below it. Remote transfers are left out because the transfer log records no root. On `stats` it needs `--full` or `--last-build` |
| `--output`, `-o` | stdout | Write to a file |
| `--top` | `10` | Number of ranked entries |
| `--redact` | off | Replace cache keys, roots, object keys, and object paths with `redacted` |

`trace`, `perfetto`, and `chrome-trace` select the same trace representation. `kache report` takes the same flags and prints the same report, for scripts written before `--full`. The JSON contract is documented by [`report.schema.json`](https://github.com/kunobi-ninja/kache/blob/main/docs/commands/report.schema.json).

`--last-build` selects by the latest compiler event timestamp across the retained log, including events older than 24 hours. It includes only that root and session ID. For local or older events without session IDs, it infers a session from overlapping compiler activity and gaps shorter than five minutes, stopping at an event with a recorded ID. The report states which rule it used. `--root` limits the search to that tree and its descendants; the selected session still belongs to one exact root.

A session can span concurrent or nearby Cargo commands. This flag does not identify one Cargo invocation. A Cargo command that does no compiler work produces no new events. Compiles still running have not been logged yet. Log rotation can remove the start of a session. An empty selection is an error.

The session report includes cache hits, misses, bypass reasons, restored bytes and the observed compiler wall span. Compile work avoided is an aggregate estimate, not elapsed build time saved. Remote transfer totals and GC are omitted because those logs cannot identify the selected session. Store inventory and deduplication totals still describe the whole cache.

JSON exposes the selection in `meta.session` (`root`, optional `session_id`, `inferred`, `inactivity_secs`); trace output carries it as `session`. In session reports, `since_secs` and `since_hours` describe the lookback to the first retained compiler start; `meta.session` defines the selection.

Raw compiler events at schema 26 add `rebuilt_package`, `rebuilt_fingerprint`, `rebuilt_path` and `rebuilt_paths` when an actual successful compile produced outputs. Unknown package metadata is omitted. `rebuilt_fingerprint` is Kache's input cache key, not Cargo's on-disk freshness hash. Cache restores and Fresh Cargo artifacts do not report a rebuild. Tagged cleanup records report removed units separately from compiler `BuildEvent` records.

Every report format totals the wrapper's phases per crate: startup, key computation, lookup, scheduler wait, restore or store, and the unattributed remainder. Startup runs from process start to wrapper entry. Key computation splits out the rustc dep-info pre-pass by time and run count. Scheduler wait is the flight join plus permit acquisition. Unattributed is wrapper overhead minus every measured phase, where overhead excludes the compile on a compiled crate and is the whole wrapper time on a hit. Events written by wrappers before schema 17 report zero startup, dep-info and wait time.

In the trace output each crate slice carries these numbers in its `args`. It nests one `X` slice per phase on the same lane, in this order: `startup`, `key` (with `dep-info` as its child), `lookup`, `wait`, `compile` on compiled crates, then `restore` or `store`. Phase offsets accumulate from the crate slice's start in that order; the wrapper records durations, not start times, so a phase sits where it lands in the sum. The gap after the last phase is the unattributed remainder. Durations are whole milliseconds, so a phase under 1 ms has no slice. A remote transfer that has both a start and a finish follows the crate slices as an async `b`/`e` pair with category `kache.transfer`.

`--redact` applies to every format. Build roots, store paths, the cache directory and your home directory are replaced wherever they appear, including inside messages such as store errors. The `telemetry/sessions.jsonl` line is written before redaction, so its root hash comes from the real root. The line holds no cache keys. Empty fields stay empty. A C or C++ row can also carry `object_output`, the path named with `-o`. A build-script row carries `package`.

## `kache telemetry`

```bash
kache telemetry write ./cache-otlp
kache telemetry write ./cache-otlp --scenario bench-firefox --phase warm
```

Writes `metrics.otlp.json` and `schema_version` for Kartero. Metric names are `kache.cache.*` / `kache.prefetch.*` (scope `kache.cache`), never `kache.bench.*`. `--scenario` must match `kache.bench.project`. `--phase` is `cold`, `warm-same-tree`, `warm`, or `pull` so dumps from the same scenario do not collide. The rustc wrapper does not write these files.

The payload also carries this host's shared cache: registered blob bytes (`kache.cache.store.physical_size`), index and WAL size, and a rowid high-water mark per table (`kache.cache.index.*`; it is not a row count). `kache.cache.store.size` counts logical entry bytes; GC compares the physical blob figure with `kache.cache.store.max`. Last-run GC gauges include duration, pinned and unreclaimable entries, bytes held by target directories, failed evictions, stale WAL snapshot failures, and recently accessed entries skipped before reading their metadata. The read-only index probe has a 25 ms lock timeout; unavailable figures are omitted.

## `kache explain`

With `[cache] explain_miss = true`, `kache explain <crate>` names compiler-reported environment variables that differ from the prior build of that unit. It compares normalized value hashes, so the log keeps variable names without their values. Older events without these hashes cannot supply the hints. The same hints appear in the JSON report.

```bash
kache explain
kache explain --root "$PWD"
kache --json explain
```

Without a crate, `explain` reads the latest recorded build and says why it missed:

- how many misses it had, against the build before it of the same tree, and the share with no recorded cause
- its misses grouped by cause, costliest compile time first, with the crates in each group
- what would help with each cause
- crates that keep missing across builds of the tree
- compilations that were not cached and why, with compiler queries counted on one line because there is nothing to cache in them

| Cause | Meaning |
| --- | --- |
| compiled but not cached | The result could not be stored, so the crate misses again next build |
| entry rejected at lookup | An entry with this key existed but could not serve the build |
| built before with this exact key, but the entry was gone | Nothing in its inputs changed; the entry was evicted or never reached this machine |
| downstream of a crate | A dependency changed and every crate built on it recompiled; the crate where the change started is named |
| own inputs changed | Its sources, compiler flags, profile or features, an environment variable it reads, its linker, or its path remapping changed |
| differs from the build in another checkout | The first build of the crate in this tree, and its inputs differ from the same crate built in another checkout. `kache explain <crate>` names the input. A path into the checkout that Kache did not map needs `KACHE_BASE_DIR`; a C or C++ library a build script generated with the checkout's path or a timestamp in it needs prefix maps and `ZERO_AR_DATE`; a flag or file that really differs needs nothing |
| no earlier compile | The first build of the crate in this tree, or older history was rotated away |
| unexplained | Nothing recorded why; `[cache] explain_miss = true` records it from the next build |

The report ends with the command that explains the costliest crate, and the one that writes the build's timeline as a Perfetto trace. `--root` limits it to builds of one tree. It exits 0; the hidden `kache diff` prints the miss counts and unexplained shares of the two builds and exits 1 when either grew, for scripts that gate on that. `kache why-miss` is the same command under its earlier name.

### One crate

```bash
kache explain serde
kache --json explain serde
```

<video src="https://raw.githubusercontent.com/kunobi-ninja/kache/main/assets/why-miss.webm" aria-label="one source edit, one recompile, and kache why-miss naming the key that changed" autoPlay loop muted playsInline controls />

Explains the latest recorded miss using the event log and stored entries. Store failures and rejected entries take precedence over key comparisons. Repeated misses under the same key in older logs retain an unknown cause when those logs cannot explain what happened.

The terminal report has the same sections as the report for the latest build: the last miss, the diagnosis, the comparison with another build tree and the dependency cascade when there is one, then the stored entries and recent events.

When the crate's newest compile was not cached, such as an edit rebuild that rustc compiled incrementally or a compile kache declined to store, `explain` reports that compile and why it was not cached instead of the older miss. JSON carries it in `latest_uncached_compile`, which is `null` otherwise.

Terminal and JSON output share the diagnosis and dependency analysis. For a recorded miss, JSON includes `dependency_chain` with changed dependencies, endpoints, and any traversal limit. An unresolved endpoint is not a confirmed cause. `dependency_recording_missing` indicates when setting `explain_miss = true` under `[cache]` and rebuilding can provide the missing dependency evidence.

The first build in a second clone or worktree has no earlier build of the crate in its own tree. In that case `explain` compares the miss with the most recent build of the same compilation unit in another build tree, usually the other checkout. The output names that tree and gives one of these verdicts:

- The crate's own input groups differ.
- Only its dependencies differ.
- Both trees computed the same key. No key input differs, so the key is not why the crate missed.

It then walks the dependency cascade through the builds the other tree used. It names the crate that changed and shows the crates above it on the `via:` path. A root whose key matches the other tree while its artifact differs is reported as not reproducible across build trees (JSON kind `path_only`). JSON carries the comparison in `checkout_comparison` and the other tree in `dependency_chain.baseline_root`.

Build trees are matched by Cargo's unit id. It is the same for a workspace member in every checkout of a project, and for a registry crate built the same way in unrelated projects. Kache records it only with `[cache] explain_miss` on, so both builds need it. A build of the crate in the same tree is always preferred.

> A crate that depends on a path crate outside the workspace root, or is built with `RUSTFLAGS` that contain the checkout path, gets a different unit id in each checkout. It has no comparison.

## `kache telemetry push`

After a build, inspect its timeline or send it to the configured planner:

```bash
kache telemetry push --dry-run
kache telemetry push --all --label prefetch=on
kache telemetry push --session SESSION_ID --label prefetch=off
```

The default selects the latest session. `--all` selects every retained session;
`--session` selects one ID. `--label KEY=VALUE` is repeatable. Set the planner
with `KACHE_PLANNER_ENDPOINT` or `[cache.planner] endpoint`, and its
credentials. A record that fails to upload prints a warning and the command
still exits 0, so a CI post-step does not fail a finished job. With no planner
configured, it says how many records it would send.

Timeline schema 7 records the session, plan, candidate source and zero-based
rank for ordinary prefetch downloads. Transfers retain `completed`,
`not_found`, `error` and `import_error` outcomes. The origin stays with a
download even if another build starts before it finishes.

When a prefetch delivered a unit's key in the same session, the unit carries a
`prefetch` object: the plan origin and rank, when the GET started, when the
entry was imported, its compressed bytes, and `timing` against the key's first
demand (`before_demand`, `in_flight` or `after_demand`). A `local_hit` with
`before_demand` timing is a prefetched local hit. The session summary counts
consumed, useful and in-flight prefetch keys and bytes, plus `get_not_found`,
`get_errors` and `get_cancelled`.

Each unit also carries `demands`, one observation per cache key looked up by
the wrapper. `first_demand_at_ms` is the first lookup's Unix timestamp in
milliseconds, including local hits. `remote_wait_ms` sums wall time blocked on
the existing daemon remote-check calls, including failed calls, IPC and daemon
admission. It does not estimate time saved. A stale predicted key and its
replacement retain separate observations. Recording adds no network request.

An orderly daemon shutdown drains prefetch tasks, then writes the active
session's local summary. A timeout aborts remaining tasks and sets `incomplete: true`;
missing transfer outcomes then remain unknown. The summary records `shutdown`
or `shutdown_timeout` as its closure reason. Shutdown does not publish an
identity manifest. Summaries written after inactivity or supersession are
snapshots, and pending tasks or log writes mark them incomplete. Complete
precision figures need a summary from an orderly shutdown, after all producers
drain.
`kache stats` marks incomplete summaries as partial totals. Forced termination
can prevent the final write.

Upgrade the service before sending schema 7 records. The updated service also
accepts schemas 1 through 6. Wrapper events before schema 20 have no demand
observations. Current demand transfers and older logs still require a unique
key or time-window match; ambiguous transfers remain unattributed.
Prefetch records each ordinary entry GET, catalog GET, pack GET and LIST once.
Background index LISTs retain the origin captured before their async work. A
completed LIST stays completed if later local index work is cancelled; its
result count and completion time describe the backend call.
`accounting.operation` distinguishes GET from LIST; `request_count` counts
backend invocations, excluding SDK retries and LIST pages. `requests_complete`
states whether that count is known. LIST records include `list_result_count`
when successful; response bytes remain unknown.

For GETs, `compressed_bytes` counts the received object body, including pack
headers and catalog metadata. `accounting.bytes_complete: false` means partial
received bytes are unknown, so a complete GET-body precision ratio cannot be
computed. A failed validation or import still retains a fully received body.
Cancellation receipts preserve known bytes and use the `cancelled` outcome.
Work cancelled before a GET starts records zero backend attempts. A 404 records
one attempt and known zero body bytes. Transport failures and interrupted bodies keep bytes unknown;
fully received bodies remain known even if decoding or import later fails.
`skipped` means another download or local entry made this candidate unnecessary;
it made no backend GET and is neutral in reports.
A bounded logging queue marks the final summary incomplete if records overflow
or cannot be written; such a run cannot establish full accounting coverage.

Pack `accounting.entries` contains each validated frame's cache key, compressed
payload bytes, immutable origin and outcome. `finished_at_ms` is the successful
local import time; zero means no import. Entries imported beyond the candidate
list have no candidate rank. Entry bytes are part of the physical pack body:
do not add them to total downloaded bytes. Useful bytes require an actual
wrapper hit in the same session and entry completion before first demand.
LIST response sizes are excluded from this GET-body measure.

For prefetch comparisons, keep cold builds and remote contents matched, label
the control run, and measure the whole CI job. Timeline compiler timings alone
do not include setup or an explicit warm step.

## `kache config`

```bash
kache config
```

This command can open even when the current config file is invalid. See [Configuration](/docs/getting-started/configuration).

## `kache login`

```bash
kache login
kache login --device
kache login --retrust
```

Logs in to the configured planner with your Kunobi account, for a planner that requires authentication. It reads the planner's issuer and client id from `/.well-known/kunobi-auth`, trusts that planner, and stores a session that the daemon refreshes. `--device` prints a URL and a code for a machine without a browser. `--retrust` accepts a planner whose login issuer changed since it was first trusted. See [Planner authentication](/docs/getting-started/configuration#planner-authentication).

## `kache logout`

```bash
kache logout
```

Forgets the stored planner session and asks the identity provider to revoke it.

## `kache test-runner`

```toml title=".cargo/config.toml"
[target.'cfg(all())']
runner = ["kache", "test-runner"]
```

Cargo runs each test binary as `kache test-runner <binary> <args>`. A test binary waits for slots in the scheduler's pool, then runs with its arguments, stdin, exit code and signals unchanged. Binaries without Cargo's hash suffix (`cargo run` targets, doctests), `--list` and `--bench` runs start at once, as does everything when the scheduler is off. Cargo is the caller, so the command is not listed in `kache --help`. See [Test runner](/docs/getting-started/configuration#test-runner).

## `kache completions`

```bash
kache completions zsh > ~/.zfunc/_kache
kache completions bash > ~/.local/share/bash-completion/completions/kache
kache completions fish > ~/.config/fish/completions/kache.fish
```

Supported shells are Bash, Elvish, Fish, PowerShell, and Zsh.

## Volume shards

With `[cache.volumes]`, `stats` and `report` sum bytes across stores and count unique cache keys. `list` and `explain` show each entry's store directories. `doctor` reports a shard with no index without creating it. `doctor --verify` checks every copy, shards included, and stops with an error at a shard that has no index. See [Volume shards](/docs/getting-started/configuration#volume-shards).