kache 0.12.0

Zero-copy, content-addressed build cache for Rust, C/C++ and more, with S3 and shared-filesystem 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
---
title: Configuration
description: Config file, environment variables, and the TUI editor.
---

# Configuration

kache reads configuration from three places, in order of priority:

1. **Environment variables** — always win, useful for CI overrides
2. **Config file** — selected from the config file priority below
3. **Defaults** — sensible values that work without any configuration

## Config file

The config file is TOML. You can edit it directly or use the TUI editor:

```sh
kache config
```

The TUI editor surfaces the common fields with their current values, marks which ones are coming from env vars (those are read-only and the cursor skips them), and lets you toggle or edit the rest interactively. Navigate with the arrows or `j`/`k`, jump between sections with Tab/Shift-Tab, Enter edits a field, Space toggles a boolean, `s` (or Ctrl-S) saves, and `q`/Esc quits with an unsaved-changes prompt. Some advanced sections — `[cache.planner]`, `[cc]`, `[paths]`, and `cache.path_only_env_vars` — have no form fields and are preserved verbatim on save.

Config file priority:

1. `KACHE_CONFIG`, when set
2. The nearest project-local `.kache.toml`, walking up from the current directory
3. User config at `~/.config/kache/config.toml`, respecting `XDG_CONFIG_HOME`

kache currently compiles two remote types: `s3` and `filesystem`.

<Tabs items={["S3", "Filesystem"]}>
  <Tab value="S3">
    ```toml title="~/.config/kache/config.toml"
    [cache.remote]
    type = "s3"
    bucket = "my-build-cache"
    endpoint = "https://s3.example.com"   # omit for AWS S3
    profile = "my-aws-profile"            # omit to use the default credential chain
    ```
  </Tab>
  <Tab value="Filesystem">
    ```toml title="~/.config/kache/config.toml"
    [cache.remote]
    type = "filesystem"
    path = "/mnt/shared-kache"
    prefix = "artifacts"
    ```

    `atomic_write_dir` is optional and defaults to
    `/mnt/shared-kache/.kache-tmp`. See [Filesystem setup](/docs/remote-cache/filesystem-setup)
    before overriding it.
  </Tab>
</Tabs>

Other OpenDAL services are not included in the kache binary. Existing S3
configuration names remain compatible: a legacy `[cache.remote]` table without
`type = "s3"` and an environment-only setup rooted in `KACHE_S3_BUCKET` still
select S3; the other `KACHE_S3_*` overrides remain available. See the
[S3 migration boundaries](/docs/remote-cache/s3-setup#opendal-migration-boundaries)
for AWS SDK-specific cases that need attention.

## All settings

| Environment variable | Config key | Default | Description |
|---|---|---|---|
| `KACHE_CACHE_DIR` | `cache.local_store` | `~/Library/Caches/kache` on macOS, `~/.cache/kache` on Linux | Local cache directory |
| `KACHE_MAX_SIZE` | `cache.local_max_size` | `50GiB` | Maximum local store size |
| `KACHE_AUTO_GC` | `cache.auto_gc` | `true` | Opportunistic size-pressure GC: after storing a new entry the wrapper runs a cheap, throttled (5 min) store-size check and spawns a detached background `kache gc` when the store exceeds `local_max_size` by more than 10%. Keeps the size cap enforced even when no daemon is running (local-only builds). Set to `0`/`false` to rely solely on daemon GC and manual `kache gc` |
| `KACHE_CONFIG` | — | — | Explicit config file path; overrides the project-local `.kache.toml` / XDG resolution |
| `KACHE_BASE_DIR` | — | — | Path prefix stripped from cache keys (collapsed to `<BASE_DIR>`) for checkout / container-mount paths the automatic sentinels don't catch — the analog of ccache's `CCACHE_BASEDIR` (see [Cache key](/docs/how-it-works/cache-key)) |
| — (file-only) | `paths.base_dirs` | `[]` | Extra absolute path prefixes for container mounts, Snap, Flatpak, AppImage, or custom roots; each maps to a distinct deterministic sentinel (see [Extra path prefixes](#extra-path-prefixes)) |
| — (file-only) | `cache.remote.type` | — | Remote type: `s3` or `filesystem`. A legacy S3 table or `KACHE_S3_BUCKET` still selects S3 when this is omitted |
| — (file-only) | `cache.remote.path` | — | Shared root directory for a `filesystem` remote |
| — (file-only) | `cache.remote.atomic_write_dir` | `<path>/.kache-tmp` | Staging directory for filesystem-remote atomic writes; must be on the same filesystem as `path` |
| `KACHE_S3_BUCKET` | `cache.remote.bucket` | — | S3 bucket name |
| `KACHE_S3_ENDPOINT` | `cache.remote.endpoint` | — | S3 endpoint URL (required for Ceph, MinIO, R2) |
| `KACHE_S3_REGION` | `cache.remote.region` | `us-east-1` | AWS region |
| `KACHE_S3_PREFIX` | `cache.remote.prefix` | `artifacts` | Object or path prefix. The environment override is retained for S3; filesystem remotes configure it in the file |
| `KACHE_S3_PROFILE` | `cache.remote.profile` | — | AWS credentials profile |
| `KACHE_S3_ACCESS_KEY` | — | — | Explicit S3 access key |
| `KACHE_S3_SECRET_KEY` | — | — | Explicit S3 secret key |
| `KACHE_CACHE_EXECUTABLES` | `cache.cache_executables` | `true` on Linux, `false` on macOS/Windows | Also cache user-facing executables (`bin` crates and `--test` binaries). dylib/cdylib/proc-macro are always cached and are unaffected by this flag. Defaults on for Linux only, where DWARF is embedded in the binary so a restored executable debugs like a fresh one; macOS (`N_OSO` records) and Windows (`.pdb` path) reference debug info outside the binary, so they stay off pending [#319](https://github.com/kunobi-ninja/kache/issues/319) |
| `KACHE_CLEAN_INCREMENTAL` | `cache.clean_incremental` | `true` | Auto-clean tracked incremental dirs during GC; active builds also remove the current crate's incremental dir eagerly. Env is on unless the value is exactly `0` or `false` (case-insensitive) |
| `KACHE_VERIFY_RESTORES` | — | `off` | Re-hash restored blobs before serving hits: `off`, `sampled` (~1/16 hits), or `always`. `1`/`true` map to `always` |
| — | `cache.exclude` | `[]` | Source-path glob patterns that bypass kache and compile normally without lookup, store, or upload |
| `KACHE_COMPRESSION_LEVEL` | `cache.compression_level` | `3` | Zstd compression level (1–22) |
| `KACHE_S3_CONCURRENCY` | `cache.s3_concurrency` | `16` | Max concurrent remote operations; the legacy S3 name applies to both remote types |
| `KACHE_PREFETCH_MAX_KEYS` | `cache.prefetch_max_keys` | `2000` | Max cache entries one prefetch plan may download; `0` = unlimited |
| `KACHE_PREFETCH_MAX_BYTES` | `cache.prefetch_max_bytes` | `2GiB` | Max compressed bytes one prefetch plan may download; `0` = unlimited. Soft cap: downloads already in flight still finish, so overshoot is bounded by the prefetch concurrency |
| `KACHE_PREFETCH_DEADLINE_SECS` | `cache.prefetch_deadline_secs` | `300` | How long a prefetch plan may keep starting downloads; `0` = no deadline |
| `KACHE_S3_POOL_IDLE_SECS` | `cache.s3_pool_idle_secs` | `300` | How long an idle S3 connection is kept in the HTTP pool. Higher values reuse warm TLS sessions across build phases; lower this if you sit behind a load balancer that drops idle connections aggressively |
| `KACHE_DAEMON_IDLE_TIMEOUT` | `cache.daemon_idle_timeout_secs` | `600` | Idle daemon shutdown timeout in seconds (`0` disables auto-shutdown) |
| `KACHE_KEY_SALT` | `cache.key_salt` | — | Opaque string folded into every cache key. Change it to force a cold cache on a toolchain change kache cannot otherwise see (see [Cache-key salt](#cache-key-salt)) |
| `KACHE_CC_EXTRA_ALLOWLIST_FLAGS` | `cc.extra_allowlist_flags` | `[]` | C/C++ flags to opt into caching that kache's built-in allow-list doesn't yet model (see [Extra cc allowlist flags](#extra-cc-allowlist-flags)). Env value is whitespace-separated |
| `KACHE_DISABLED` | — | `false` | Disable caching entirely (pass-through to rustc). Env-only, no config key: `1` or `true` (case-insensitive) disables; any other value — including `0`, `false`, or unset — leaves caching on |
| `KACHE_LOCAL_ONLY` | `cache.local_only` | `false` | Strict local-only mode: ignore **all** remote and planner config/env (no configured remote, no planner endpoint, no egress) for a guaranteed-hermetic build. Local caching stays fully on — unlike `KACHE_DISABLED`. `1`/`true` enables; env wins over the file (an explicit `0` overrides `local_only = true`) |
| `KACHE_REMOTE_READONLY` | `cache.remote_readonly` | `false` | Read-only remote consumer mode: when enabled, kache performs remote cache reads/restores as normal, but suppresses all remote uploads (including the sync push phase and manifest saving). `1`/`true` enables; env wins over the file (an explicit `0` overrides `remote_readonly = true`) |
| `KACHE_WINDOWS_HARDLINK` | `cache.windows_hardlink` | `false` | Windows only: restore cache hits on non-CoW volumes (NTFS) via hardlink instead of copy, deduplicating the working tree against the store. Opt in **only** if your build never deletes or rewrites a restored output in place — a mutation would corrupt the shared store blob. ReFS (Dev Drive) volumes always block-clone regardless of this flag |
| `KACHE_STORAGE_LAYOUT_ADVICE` | `cache.storage_layout_advice` | `true` | Surface an advisory (deduplicated, at most once per 5-minute window) when a cache hit is restored by copy because the storage layout prevents zero-copy dedup: no copy-on-write on the volume, cache and build tree on different volumes, or an inconclusive capability probe. Set to `0`/`false` when the layout is intentional (e.g. an NTFS-only machine that cannot host a ReFS Dev Drive); genuine clone faults are still reported |
| `KACHE_HEARTBEAT_SECS` | `cache.heartbeat_secs` | `30` | In-flight compile heartbeat cadence: while a cache-miss compile runs longer than one cadence, kache prints `still compiling <crate> — 4m20s elapsed (typical: 7m51s, ETA 3m31s)` to stderr and appends a structured `heartbeat` line to `events.jsonl` (for tooling that cannot rely on TTY output). The typical/ETA figures come from the median of the crate's recent recorded compile times. `0` disables both sinks |
| `KACHE_EXPLAIN_MISS` | `cache.explain_miss` | `false` | Miss diagnostics: on a cache miss for a crate that previously hit in the same build tree, print which key input group changed (`key changed in: args, env_deps`) and record it on the miss event. Costs one event-log read per miss, so enable it only while investigating unexpected misses |
| — (file-only) | `cache.ignore_env` | `false` | Make the config file authoritative by ignoring `KACHE_*` env overrides for file-backed settings (see [Pinning config against env](#pinning-config-against-env)) |
| `KACHE_FALLBACK` | `cache.fallback` | — | Secondary compiler-wrapper to hand passed-through compiles to; kache runs `<fallback> <compiler> <args>` when it declines to cache. `off`/`none`/empty disables |
| `KACHE_PATH_ONLY_ENV_VARS` | `cache.path_only_env_vars` | `[]` | Extra env vars (besides `OUT_DIR`) whose values only locate an `include!`'d file, so kache normalizes their absolute path in the cache key. Env value is comma/whitespace-separated and replaces the file list (see [Path-only env vars](#path-only-env-vars)) |
| `KACHE_PLANNER_ENDPOINT` | `cache.planner.endpoint` | — | Prefetch-planner service URL; setting it enables the planner client. Empty/whitespace is treated as unset |
| `KACHE_PLANNER_TIMEOUT_MS` | `cache.planner.timeout_ms` | `750` | Planner request timeout in milliseconds |
| `KACHE_PLANNER_TOKEN` | `cache.planner.token` | — | Bearer credential sent with planner requests |
| `KACHE_NAMESPACE` | — | — | Enables content-addressed shard uploads/prefetch (requires a `Cargo.lock`); the `kache save-manifest --namespace` flag takes precedence. Unset uploads only the monolithic manifest |
| `KACHE_EVENT_ROOT` | — | auto-detected | Explicit root path stamped on wrapper events for report filtering; useful for benchmark harnesses that share a cache/event log |
| `KACHE_LOG` | — | `kache=warn` * | Log level for stderr output |
| `KACHE_LOG_FILE` | — | `kache=info` * | Log level for the file log |
| `KACHE_PROGRESS` | — | (off) | Per-crate progress lines to stderr: `1`/`hits` prints cache hits only; `verbose`/`all` also prints dups and misses; unset or anything else stays silent |

\* In wrapper mode (`RUSTC_WRAPPER`, the common path) stderr logging defaults to **off** and the file log is **disabled** unless `KACHE_LOG_FILE` is set explicitly. The `kache=warn` / `kache=info` defaults apply to CLI and daemon invocations.

## Excluding Sources

Use `cache.exclude` when a source file should compile normally but never use kache:

```toml title=".kache.toml"
[cache]
exclude = [
  "crates/problematic-rust-crate/**",
  "vendor/problematic-c-lib/**",
  "$CARGO_HOME/registry/src/**/some-crate-*/**",
]
```

Patterns are globs matched against the compiler's primary source path. Relative patterns are matched relative to the current build directory and, when kache can infer it, the Cargo workspace root. Excluded invocations bypass local lookup, remote lookup, store, and upload.

Exclude patterns support `~`, `$VAR`, and `${VAR}` expansion. `$CARGO_HOME` falls back to Cargo's default `~/.cargo` when the environment variable is not set, so registry-source exclusions work on default Cargo installs.

## Pinning config against env

By default every `KACHE_*` environment variable **wins** over the config file — convenient for CI overrides, but it means a stray machine-global export can silently change behavior. The riskiest case is `KACHE_KEY_SALT`: a leaked export would shift *every* cache key without a word.

Set `ignore_env` to make a pinned config authoritative:

```toml title=".kache.toml"
[cache]
ignore_env = true
key_salt = "v3-toolchain-abc"
```

With this, kache ignores the `KACHE_*` overrides for **file-backed settings** (cache dir, max size, key salt, fallback, remote/planner settings, the toggles, etc.) and takes the file value (or built-in default) instead. When an ignored override is actually set, kache logs a warning naming it, so the suppression is never silent.

`ignore_env` is **file-only** by design — an env var can't re-enable env overrides, or the lockdown would be trivially undone by the same stray export it defends against. It deliberately does **not** cover bootstrap/operational vars that have no file representation (`KACHE_CONFIG`, `KACHE_DISABLED`, `KACHE_LOG` / `KACHE_LOG_FILE` / `KACHE_PROGRESS`, `KACHE_NAMESPACE`, `KACHE_BASE_DIR`) or S3 credentials (`KACHE_S3_ACCESS_KEY` / `KACHE_S3_SECRET_KEY`), which are secrets rather than config.

## Cache-key salt

kache's cache key captures everything it can *observe* about a compile: the rustc version, the target, flags, source and dependency hashes, and the linker's `--version` banner. Native Linux OS-loaded outputs also key the host GNU libc or musl version, while portable rlibs and cross-target outputs exclude that host-only signal. The observable set is **not** complete for toolchain changes that leave every reported version unchanged — for example a custom or cross-target libc/sysroot replacement, a distro patch that retains the same upstream libc version, a hidden `mold`/linker change, or a Nix store rebuild that swaps the ELF interpreter baked into a binary.

When that happens, the key does not move, so a stale artifact can be restored. On Nix in particular, a `nixpkgs` bump followed by `nix store gc` can leave a restored executable pointing at a garbage-collected interpreter — an error no `cargo clean` can fix, because the key never changed.

`key_salt` is an opaque string folded into every cache key. Set it to a value that *does* change when your toolchain changes — a hash of the toolchain closure, a store-path digest, a date, a CI build id — and that change re-keys the cache (a cold miss) instead of serving a stale hit:

```toml title=".kache.toml"
[cache]
key_salt = "nixpkgs-a1b2c3d-mold-2.40"
```

Or compute it from the toolchain itself:

```sh
export KACHE_KEY_SALT="$(nix eval --raw .#devShells.default.outPath | sha256sum | cut -c1-16)"
```

The salt is hashed raw; its meaning is entirely yours. An unset or empty value has **no effect** — keys are byte-identical to not setting it — so it is safe to leave off until you need it. A misconfigured salt can only cost hit rate (an extra miss), never cause a wrong artifact to be restored.

## Extra cache-key inputs

kache keys a crate on what the compiler reports — source files, `--extern` dependencies, flags. Some crates also read files **at compile time that the compiler never reports**, so kache can't see them change:

- sqlx's `query!` macro reads `.sqlx/query-*.json` (the offline query cache)
- migration macros read `migrations/`
- codegen / macros that `include!` data files by a path rustc doesn't surface

Edit one of those and the `.rs` files are unchanged, so the key doesn't move — and kache would restore the previously-compiled artifact (a stale hit). Declare those files so a change to them re-keys the crate.

Add a `kache.toml` next to the crate's `Cargo.toml`:

```toml title="my-app/db/kache.toml"
extra_inputs = [
  ".sqlx/**/*.json",
  "migrations/**/*.sql",
]
```

Globs are relative to the crate directory. A bare directory is fine — `.sqlx` and `.sqlx/` both mean "everything under it." The matched files' contents are folded into that crate's key.

A pattern **may** reach outside the crate with `..` or an absolute path when a build genuinely depends on a shared tree above the crates or a machine-specific file. That's allowed and stays fail-safe, but it makes the crate's key host- or layout-specific (kache logs a warning), so it no longer shares across machines or worktrees — prefer a co-located input where you can.

This is **opt-in and scoped per crate**: a crate with no `kache.toml` is unaffected, and one crate's `extra_inputs` never *implicitly* touch a sibling crate's key. It is also **union-only** — a misdeclared glob can only cause an extra cache *miss* (a rebuild), never restore a wrong artifact. Each file is folded as its crate-relative path plus content hash, so moving the worktree doesn't bust the cache, but swapping two matched files' contents (where order matters, like sqlx migrations) does re-key. The declared patterns are folded too, so editing `kache.toml` itself re-keys — even when it currently matches nothing.

<Callout type="warn">
  Keep patterns narrow. A glob that walks a huge tree (an absolute `/**`, or a
  `**/*` that accidentally spans `target/`) re-keys on every change and re-walks
  the tree on every compile — kache warns when a pattern matches the filesystem
  root or an unusually large number of files.
</Callout>

<Callout type="warn">
  `extra_inputs` is only evaluated **when cargo invokes the compiler**. Editing a
  tracked file in a warm in-place target — changing `.sqlx/` without touching any
  `.rs` — doesn't re-invoke rustc on its own, so the key isn't recomputed and the
  prior artifact is restored. To make such edits re-key, your build script must
  emit a matching `cargo:rerun-if-changed` for the path (cargo then re-runs the
  crate when it changes). `kache doctor` flags crates that declare `extra_inputs`
  but whose build script won't re-trigger the compiler.
</Callout>

<Callout type="info">
  `kache.toml` (no leading dot) is **only** for `extra_inputs`. It is distinct
  from the project config `.kache.toml`; any other key in it is a loud error.
</Callout>

## Extra path prefixes

Use the file-only `[paths].base_dirs` list when automatic workspace/home/temp
detection and the legacy single `KACHE_BASE_DIR` do not cover paths baked into
artifacts—for example container mounts, `/snap`, `/var/lib/flatpak`, an
AppImage mount, or a custom toolchain root:

```toml title=".kache.toml"
[paths]
base_dirs = ["/snap", "/var/lib/flatpak", "/work"]
```

Entries must be absolute and contain no `..`. They need not exist on every
machine, so a shared project config remains usable outside the container or
sandbox. kache sorts the normalized entries independently of TOML order,
assigns each one a distinct `<BASE_DIR_N>` key sentinel and
`/kache/base-dir-N` compiler path, and uses the longest configured prefix when
entries overlap. The configured roots feed both rustc and gcc/clang key inputs
and emitted prefix maps. `KACHE_BASE_DIR` remains supported as the separate
legacy `<BASE_DIR>` rule.

<Callout type="warn">
  Path normalization removes distinctions and therefore merges cache keys. An
  over-broad root, or differently paired lists on two machines, can make
  genuinely different inputs look identical and restore a wrong artifact.
  C/C++ compiler prefix maps use raw byte-prefix semantics: for example,
  `/work` also matches `/workspace`. Rust mappings are path-component aware.
  Choose narrow, unambiguous roots, commit the same `.kache.toml` for every
  cache participant, and review this warning and kache's per-entry audit log
  when this list is active.
</Callout>

## Path-only env vars

By default kache normalizes the absolute path baked into `OUT_DIR` so the cache key stays portable across machines and worktrees. Some builds set other env vars that serve the same role — they only point at a generated file that a crate `include!`s, and their absolute value should not bust the key (e.g. Firefox's `BUILDCONFIG_RS` / `MOZ_TOPOBJDIR`).

`cache.path_only_env_vars` opts those vars into the same path-only treatment:

```toml title=".kache.toml"
[cache]
path_only_env_vars = ["BUILDCONFIG_RS", "MOZ_TOPOBJDIR"]
```

Or via the environment (comma- or whitespace-separated; replaces the file list entirely):

```sh
export KACHE_PATH_ONLY_ENV_VARS="BUILDCONFIG_RS MOZ_TOPOBJDIR"
```

This is an advanced opt-in: only list vars whose value is purely a path locator. An empty list means only `OUT_DIR` is normalized.

## Extra cc allowlist flags

kache caches a C/C++ compile only when it recognizes every flag on the command line. Its cc flag classifier is an **allow-list**: each flag is one kache has reasoned about and knows how to key. Anything unrecognized is refused and the compile passes through uncached — the safe default, since a flag kache doesn't model could change the object file without changing the key.

For setup and the current C/C++ support matrix, see [C/C++ caching](/docs/getting-started/c-cpp).

**Supported compilers and dialects.** kache wraps the GNU-dialect drivers (`cc`, `c++`, `gcc`, `g++`, `clang`, `clang++`; POSIX) and the MSVC-dialect `clang-cl` (Windows, or `clang --driver-mode=cl`). The two dialects have separate allow-lists, so each flag is classified in the spelling its compiler actually uses — e.g. `-fno-rtti` (GNU) vs `-GR-` (clang-cl), `-std=c++20` vs `-std:c++20`. For clang-cl, debug info (`/Z7` / `-Z7` / `-g`) **is** cached (machine-local — clang-cl embeds CodeView paths in the `.obj`), while `-bigobj` and `-showIncludes` are still deliberately **refused** (passed through) for now; the common MSVC codegen set (`-EH*`, `-GR`/`-GS`, `-guard:`, `-std:`, `-fms-compatibility-version=`, `/O*`, `-Gy`/`-Gw`, `-MD`/`-MT`, …) is modeled. `extra_allowlist_flags` entries below are matched against command-line tokens verbatim, so list them in whichever dialect your compiler speaks (a `/`-spelled clang-cl flag included).

The cost is that a common-but-unlisted flag disables caching for that compile until kache ships a release adding it. `cc.extra_allowlist_flags` lets you opt such a flag in locally, ahead of official support:

```toml title=".kache.toml"
[cc]
extra_allowlist_flags = ["-ffunction-sections", "-fdata-sections", "-fno-rtti"]
```

Or via the environment (whitespace-separated; overrides the file):

```sh
export KACHE_CC_EXTRA_ALLOWLIST_FLAGS="-ffunction-sections -fdata-sections"
```

Semantics:

- **Exact match.** An entry matches a command-line token character-for-character — no prefixes or wildcards. List each value you use (e.g. `-march=armv8.2-a`, not `-march=`).
- **Hashed verbatim.** A matched flag that's actually present is folded into the cache key as its literal string, so a different flag (or value) always produces a different key — it can never miscache *by value*.
- **Add-only.** This can only make kache *stop refusing* a flag. It cannot override structural refusals — link mode, coverage instrumentation, multi-`-arch`, precompiled headers, modules, and the like still pass through.
- **Off by default.** An empty list has no effect; keys are byte-identical to not setting it.

<Callout type="warn">
Avoid host-dependent flags like `-march=native`. The string is identical on every machine but compiles to different objects per CPU, so hashing it verbatim collides across hosts — a cache hit on one machine can restore an object built for another. List explicit architectures instead.
</Callout>

To confirm it took effect, run with `KACHE_LOG=kache=debug`: the cc flag-classify summary gains a `user-allowed` count, and the flag no longer appears in any `unsupported flag(s): … — passthrough` line. At `kache=trace` each accepted flag logs as `user-allowed (config)` and each one folded into the key logs as `cc_extra_flag=`.

## S3 credential resolution order

Remote S3 credentials are resolved in this order:

1. `KACHE_S3_ACCESS_KEY` + `KACHE_S3_SECRET_KEY`
2. Standard `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
3. Static credentials in the selected profile (`KACHE_S3_PROFILE`, `cache.remote.profile`, `AWS_PROFILE`, then `default`)
4. Legacy inline SSO, then `credential_process` for the selected profile
5. Environment-based web identity, ECS/task credentials, then EC2 instance credentials

For Ceph, MinIO, or R2, set `cache.remote.endpoint` or `KACHE_S3_ENDPOINT`.

```toml title="Ceph example"
[cache.remote]
type = "s3"
bucket = "build-cache"
endpoint = "https://s3.example.com"
profile = "ceph"
```

## Composing with other compiler wrappers

kache works as a `RUSTC_WRAPPER` and composes with a second wrapper in either direction.

**Behind another workspace wrapper** (`RUSTC_WORKSPACE_WRAPPER`, e.g. `clippy-driver`): when cargo has both `RUSTC_WRAPPER=kache` and a `RUSTC_WORKSPACE_WRAPPER` set, it invokes `kache <workspace-wrapper> <rustc> <args>`. kache detects that the inner argument is itself a compiler, forwards the real rustc path as the workspace wrapper expects, and keys/caches around it. `cargo clippy` (which drives `clippy-driver`) is handled the same way — no extra configuration needed.

**In front of another wrapper** — when you want a second wrapper to cache the compiles kache *declines*: set [`KACHE_FALLBACK`](#all-settings) (or `cache.fallback`). kache then runs `<fallback> <compiler> <args>` for passed-through invocations, so e.g. sccache gets a chance at the compiles kache won't cache.

<Callout type="info">
  `KACHE_DISABLED=1` makes kache a transparent shim (no lookup, store, or
  upload) while still stripping incremental flags — useful to temporarily
  bypass kache in a wrapper chain without unsetting the env var. Only `1` or
  `true` (case-insensitive) disable; `0`/`false`/unset leave caching on.
</Callout>

## Restore verification

By default, a hit checks recorded metadata and blob size, then restores the blob. Set `KACHE_VERIFY_RESTORES=sampled` to content-hash roughly one in sixteen hits, or `KACHE_VERIFY_RESTORES=always` to hash every restored blob before serving it:

```sh
KACHE_VERIFY_RESTORES=sampled cargo build
```

`sampled` is useful as cheap background coverage for silent disk corruption. `always` is stricter but adds an extra full read of every restored blob.

## Size values

Size fields (`KACHE_MAX_SIZE`, `cache.local_max_size`) accept human-friendly strings:

```
50GiB   10GB   512MiB   1024MB
```

## Log levels

`KACHE_LOG` follows the `tracing` subscriber syntax:

```sh
KACHE_LOG=kache=debug   # verbose, useful when diagnosing cache misses
KACHE_LOG=kache=info    # operational detail
KACHE_LOG=kache=warn    # default — only surface real problems
```

The file log is written to `~/Library/Logs/kache/kache.log` on macOS and `~/.cache/kache/kache.log` elsewhere. It rotates automatically when it exceeds 5 MB.

## Local store layout

```
<cache_dir>/               # Linux: ~/.cache/kache · macOS: ~/Library/Caches/kache · Windows: %LOCALAPPDATA%\kache
├── store/
│   ├── blobs/
│   │   └── ab/
│   │       └── abcdef...   # content-addressed blob (blake3), stored once
│   └── <cache_key>/
│       └── meta.json       # entry metadata; references its blobs by hash
├── index.db                # SQLite index (WAL mode) — sibling of store/, not inside it
├── events.jsonl            # build event log
├── transfers.jsonl         # transfer log
└── daemon.sock             # daemon IPC socket (a named pipe on Windows)
```

The whole cache directory is excluded from Time Machine and Spotlight on macOS automatically — on first start the daemon sets a `tmutil` exclusion and a `.metadata_never_index` sentinel.

## Containers and cross-compilation

kache's index (`index.db`) is a SQLite database, and SQLite needs reliable file locking from the filesystem it lives on — plus shared memory, in WAL mode. A normal local cache directory provides both. A cache directory **shared across machine or OS boundaries** does not.

The usual way to hit this is bind-mounting your host cache directory into a build container — for example [`cross`](https://github.com/cross-rs/cross), Docker, or Podman:

- The directory is mounted from the host into the container, so `index.db` is opened from two different operating systems at once.
- A kache daemon on the host typically holds `index.db` open in WAL mode for the duration of the build, and that WAL state cannot be shared with the process inside the container.

When kache can't open the index it falls back to building **uncached** — your build still succeeds, just with no cache hits or stores — and prints a one-time warning:

```
[kache] the cache index could not be opened after retries (...).
[kache] Caching is disabled for this build — compilation still succeeds,
[kache] just without cache hits or stores (everything builds uncached).
[kache] ...
[kache] → set KACHE_CACHE_DIR to a fast, local, single-machine path
```

**Give the container its own cache directory.** Point `KACHE_CACHE_DIR` at a path the container alone uses — ideally a dedicated volume so the cache persists across runs:

```sh
# inside the container / cross build — a container-local volume, NOT the host's cache dir
export KACHE_CACHE_DIR=/kache-cache
```

Don't bind-mount the host's cache directory into the container. Sharing it gains little anyway: host and cross builds target different platforms and feature sets, so they produce different cache keys and would not reuse each other's entries.

<Callout type="info">
  The same rule applies to network filesystems (NFS, SMB/CIFS, 9p) and any
  setup where more than one machine touches the same directory: keep
  `KACHE_CACHE_DIR` on a fast, local disk owned by a single machine. To reuse
  build artifacts *across* machines, configure an [S3 remote](/docs/remote-cache/s3-setup)
  or a separate [filesystem remote](/docs/remote-cache/filesystem-setup). Only
  the filesystem remote's `path` belongs on the shared mount.
</Callout>