Configuration reference
trimwire install writes a commented starter config and turns the workhorse
strategies on. This file documents every knob. You rarely need most of them.
Where config lives (two-tier + env)
Section titled “Where config lives (two-tier + env)”Resolved in this order, later wins:
- Global:
$XDG_CONFIG_HOME/trimwire.toml, falling back to~/.config/trimwire.toml. - Per-project:
./.trimwire.tomlin the working directory (overrides global; lists are replaced, not merged). - Environment:
TRIMWIRE_*variables, nested with__. E.g.TRIMWIRE_SERVER__LISTEN=127.0.0.1:9000,TRIMWIRE_STRATEGIES__BLOAT_CAP__THRESHOLD_BYTES=32768.
Edit the global config quickly with trimwire config.
Per-session control (one Claude Code on, another off)
Section titled “Per-session control (one Claude Code on, another off)”trimwire routes Claude Code through the gateway via the ANTHROPIC_BASE_URL env
var that trimwire install adds to your shell rc. Activation is therefore
per-shell, so running it for one Claude Code session but not another already
works — no global toggle needed:
- Bypass trimwire for ONE session (gateway stays up for everyone else):
trimwire run --bypass -- <claude args>— it points that oneclaudestraight at Anthropic (no pruning) without touching global state. (Equivalent by hand:ANTHROPIC_BASE_URL= claude …orunset ANTHROPIC_BASE_URLthenclaude.) - Use trimwire for ONE session without the always-on service:
trimwire run …— it scopesANTHROPIC_BASE_URLto just that child process (reusing the running gateway if one is up, else starting a private one) and tears it down afterward.
So “active for this session, off for that one” is just which shells export
ANTHROPIC_BASE_URL, or a single trimwire run --bypass. (Note: trimwire pause globally puts the gateway in bypass — it keeps serving but forwards
unmodified, so every shell keeps working with no pruning; trimwire resume
resumes. It does not edit your rc. trimwire off, by contrast, fully
disengages: it stops the gateway and removes the ANTHROPIC_BASE_URL wiring so
Claude Code talks directly to Anthropic — use it when you need a host-gated
feature like Remote Control (or, to keep pruning too, enable [server] remote_control — see below); trimwire on re-engages.)
profile — the one knob most people need
Section titled “profile — the one knob most people need”profile = "default" # "default" | "gentle"A profile seeds every strategy knob below it, so you usually don’t touch the
[strategies.*] tables at all. It sits below your explicit keys in the merge
order; anything you set by hand still wins.
| profile | strategies | who it’s for |
|---|---|---|
default (shipped) |
All eight cache-safe strategies with aggressive knobs: mostly keep_recent_turns=2 (a few wider — stale_reads/thinking_strip keep 4), bloat cap 4 KB, image keep 1, verb-class denylist for browser tools, plus stale_input_cap, stale_reads, thinking_strip, and reprune on. (Exact per-strategy values are in the sections below.) |
Most people; especially long or tool-heavy sessions. Cleanest context, kept cache-stable by reprune. |
gentle |
cross_turn_dedup + failed_input_purge + conservative bloat_cap (32 KB / keep 6) + thinking_strip (keep 8) + reprune. stale_input_cap, stale_reads, sliding_window, and image_strip are off. |
Lightest-touch option; least pruning, least rot protection. |
Both profiles turn on stable-prefix re-pruning ([reprune]), which keeps the
pruned prefix byte-identical between checkpoints so the prompt cache survives.
That’s what makes the aggressive default cache-safe. Cost behaviour across
profiles is non-monotonic (more pruning is not always lower cost); see
benchmark/ for the numbers. An unrecognised name falls back to default with
a warning. Setting profile in a project ./.trimwire.toml is allowed (it only
affects pruning; upstream stays global-only).
[server]
Section titled “[server]”[server]listen = "127.0.0.1:8765" # numeric IP:port the gateway bindsupstream = "https://api.anthropic.com" # where slimmed requests are forwardedlisten must be a numeric IP:port (e.g. 127.0.0.1:8765 or [::1]:8765),
not a hostname — localhost:8765 is rejected (it falls back to the default with
a warning). If you change listen, set ANTHROPIC_BASE_URL to match.
remote_control — pruning and Remote Control on the same session (opt-in, off)
Section titled “remote_control — pruning and Remote Control on the same session (opt-in, off)”Claude Code refuses to start Remote Control (driving your session from your
phone / claude.ai) whenever ANTHROPIC_BASE_URL points at a non-Anthropic host —
so trimwire’s normal wiring and Remote Control are otherwise mutually exclusive.
[server]remote_control = trueWith this on, trimwire install / trimwire on wire trimwire a different way:
instead of exporting ANTHROPIC_BASE_URL, they export
BUN_OPTIONS=--preload ~/.trimwire/coexist-shim.js and leave the base URL unset
(so Remote Control’s gate is satisfied). A tiny preload shim inside Claude Code
reroutes only POST /v1/messages through the gateway for pruning; everything else —
including the Remote Control channel — goes straight to Anthropic. Re-run
trimwire on after changing this, and open a new shell (or source your rc).
Opt-in and fragile, by design. It relies on Claude Code’s Bun runtime + fetch
internals — it works today, but a Claude Code update can break it (it fails safe,
falling back to going direct) — and it works around a deliberate Anthropic
restriction. All traffic still goes only to Anthropic; trimwire just prunes
context, exactly like the default path. Requires a loopback listen (trimwire
refuses to wire this mode for a non-local gateway). trimwire doctor verifies the
wiring.
GUI/editor-launched Claude Code. The shell rc only reaches terminal-launched
sessions. For Claude Code launched outside a shell (the VS Code panel, the desktop
app), trimwire install/on also writes a process-local launcher at
~/.trimwire/claude-launch.sh — it execs the real claude with the same preload
set only in that process (no session-global env var, so no other Bun process is
touched). Point your editor’s Claude “process wrapper” at it: in VS Code set
claudeCode.claudeProcessWrapper to ~/.trimwire/claude-launch.sh (Remote-WSL/SSH:
put it in ~/.vscode-server/data/Machine/settings.json on the remote host). The
launcher execs whatever binary the extension hands it, which works when the extension
bundles a Claude binary for your platform (the common macOS/Linux case); on a platform
with no bundled binary the extension passes only CLI args, so point the setting at a
real claude binary directly instead. Terminal and JetBrains sessions already prune
via the rc block. See also the
FAQ entry.
Strategies
Section titled “Strategies”They run in this fixed order: failed_input_purge → stale_input_cap →
cross_turn_dedup → stale_reads → simhash_dedup → bloat_cap →
sliding_window → image_strip → thinking_strip. Each has an enabled
flag. simhash_dedup is the only one OFF in both shipped profiles (opt-in);
thinking_strip is ON in both (its struct default is off, but the profiles
enable it). Tool-name lists support * globs. trimwire stats shows which
fire and how much each saves.
[strategies.cross_turn_dedup] — workhorse, on by default
Section titled “[strategies.cross_turn_dedup] — workhorse, on by default”Same tool called again with identical arguments (re-reading a file, re-running a grep) → keep only the most recent result; replace earlier identical ones with a marker. Safe: only provably-superseded duplicates are removed.
[strategies.cross_turn_dedup]enabled = true# exempt_tools = ["Task", "Agent"] # tools never deduped (default: subagent tools)# stub = "[trimwire: superseded by a later identical call]"[strategies.failed_input_purge] — workhorse, on by default
Section titled “[strategies.failed_input_purge] — workhorse, on by default”For tool calls whose result errored, reduce the (often large) input after
a few turns: keep the small scalar fields (command, file_path, flags up to
~512 B) so the model still knows which call failed, and replace only the bulk
(large strings and nested arrays/objects: heredocs, file bodies, stdin) with
a content-free size marker. A small input is left untouched. The tool name, id,
and the error text are always preserved.
[strategies.failed_input_purge]enabled = truekeep_recent_turns = 4 # struct default; the `default` profile sets this to 2# exempt_tools = [][strategies.bloat_cap] — on by default
Section titled “[strategies.bloat_cap] — on by default”Trim a single oversized tool_result (e.g. a huge build log) to
head + tail + a [trimwire: trimmed N bytes] marker, but only once it’s older
than keep_recent_turns. Skipped if the trim wouldn’t actually shrink it.
Never touches file-editing tools. For array-content results (e.g. multi-block
MCP output) it salvages the bulky text blocks in place (same head/tail trim),
keeping structure, small blocks, and images; a pure non-text/image array over
threshold is replaced by a single size marker.
Subagent (Task/Agent) results are age-gated on a wider window (not exempt
at every age). A subagent’s findings/blocker list is consumed across many follow-up
turns, so it stays exempt while within subagent_keep_recent_turns (default 8, vs
the tight global keep_recent_turns), then head+tail-salvaged like any old result —
top findings + conclusion kept, the dense middle trimmed. Add Task/Agent back to
exempt_tools to restore the legacy all-ages exemption (this is what the gentle
profile does). Authoring results (Write/Edit/MultiEdit) stay all-ages exempt.
Read gets its own, wider recent window (exempt_recent_only_keep_turns, default
4 in the default profile). A medium file read (4–16 KB) is protected by stale_reads
for 4 turns but was head+tail-trimmed by bloat_cap after only 2 — so its middle
vanished at age 3 while the model might still reference it. Matching this to
stale_reads’ 4-turn window closes that gap (a recoverable trim — the file is
re-readable — so the tradeoff is read-heavy savings, not correctness). The effective
window is max(exempt_recent_only_keep_turns, keep_recent_turns); 0 falls back to the
global window. Bash/MCP results keep the tight global window (savings preserved).
[strategies.bloat_cap]enabled = truethreshold_bytes = 16384 # struct default; the `default` profile sets this to 4096head_bytes = 2048 # bytes kept from the starttail_bytes = 2048 # bytes kept from the endkeep_recent_turns = 4 # struct default; the `default` profile sets this to 2exempt_tools = ["Edit", "Write", "MultiEdit"] # never trimmed at ANY age (authoring — eliding them corrupts sessions)subagent_keep_recent_turns = 8 # Task/Agent results exempt within this WIDER window, then head+tail-salvaged once oldexempt_recent_only_tools = ["Read"] # exempt only while RECENT; an OLD large Read IS trimmed (the "Read coverage gap" fix)exempt_recent_only_keep_turns = 4 # Read's own recent window (>= keep_recent_turns); the `default` profile sets 4; 0 = fall back to keep_recent_turns# --- opt-in levers (all default 0 / empty = OFF; zero behaviour change unless set) ---catastrophic_bytes = 0 # if >0, also caps a RECENT result this large (it can't # fit the context window anyway, so it would brick the # session). Set it WELL ABOVE threshold_bytes. Uses a # generous head/tail floor and a distinct marker.stub_age_turns = 16 # the `default` profile sets this to 16 (struct default 0). # A string result older than this is FULLY stubbed to a # marker (not head+tail). Must exceed keep_recent_turns or # it has no effect. String content only. See the note below.protected_file_patterns = [] # globs of file paths never trimmed (mirror the same # globs in [strategies.stale_reads] for full protection)The age ladder (stub_age_turns, on at 16 in default). Once a string result is
older than stub_age_turns assistant turns, it’s replaced by a bare
[trimwire: aged-out result N bytes] marker instead of a head+tail trim — it keeps the
size marker but loses the head/tail glimpse and the salvaged error/warning signal
lines. This is a concentrated win on long result-heavy sessions (the ones past 16
turns, where trimwire’s value is highest) and has zero effect on short sessions. The
tradeoff is fidelity on very-old results: a Read is re-readable, but an old Bash/MCP
output stubbed this way is gone. 16 (rather than a tighter value) deliberately keeps the
12–16 turn band as head+tail so a still-referenced recent-ish error survives. Set it to
0 to disable (very-old results then keep their head+tail). gentle leaves it off.
[strategies.sliding_window] — most aggressive, browser-only by default
Section titled “[strategies.sliding_window] — most aggressive, browser-only by default”Stub denylisted tool_use/tool_result pairs older than keep_recent_turns,
regardless of whether they’re duplicates. More aggressive than dedup, so it
defaults to browser-automation tools only. Add e.g. "Bash"/"Grep" only if
you want all their old results dropped (this can remove a unique result the
model still wanted; dedup is the safer general win).
[strategies.sliding_window]enabled = truekeep_recent_turns = 4 # struct default; the `default` profile sets this to 2# the shipped `default` value — verb-class globs matching browser automation# (`*browser_act*`, NOT a bare `*act*`, which would also match extract/interact/redact):denylist_tools = ["*screenshot*", "*navigate*", "*click*", "*browser_act*", "Grep"]exempt_tools = ["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Task", "Agent"] # never stubbed, even if denylisted# stub = "[trimwire: elided, older than sliding window]"[strategies.image_strip] — on by default
Section titled “[strategies.image_strip] — on by default”Replace base64 image payloads (e.g. screenshots, accessibility/DOM/heap snapshots) older than the K most-recent matching results with a marker.
[strategies.image_strip]enabled = trueapplies_to_tools = ["*screenshot*", "*snapshot*"] # screenshots + snapshot/heapsnapshot/DOM image blobskeep_recent_count = 3 # struct default; the `default` profile sets this to 1# stub = "[trimwire: image stripped]"[strategies.stale_input_cap] — on in default, off in gentle
Section titled “[strategies.stale_input_cap] — on in default, off in gentle”Caps the bulky input of an old successful tool call (the failed-input
counterpart is failed_input_purge). Protects the most recent turns.
Authoring tools (Write/Edit/MultiEdit/NotebookEdit) are age-gated with a
recoverable marker, not permanently exempt. Their authored body is kept verbatim
while recent — within the wider authoring_keep_recent_turns window (default 6 vs.
2 for ordinary inputs) — so content the model is actively editing is never touched.
Once it ages past that window it is replaced with [trimwire: wrote <path> (<N>B) — read the file to restore] instead of the generic size marker: the call succeeded,
so the content is on disk and the model re-reads to recover it. A failed authored
call’s body is never touched here (it never hit disk — that floor stays in
failed_input_purge).
Subagent (Task/Agent) prompts are reduced once old and successful — no
default exemption. After a subagent call succeeds its result captures the outcome,
so the verbatim sub-task prompt is dead weight; the short description is kept and
only the bulky prompt is elided to a size marker. (A failed subagent call keeps
its exemption in failed_input_purge, where a retry may re-emit the prompt.) Add a
tool name back to exempt_tools if you want to keep its input verbatim.
[strategies.stale_input_cap]enabled = truekeep_recent_turns = 2 # ordinary inputs (Bash stdin, MCP args, subagent prompts)authoring_keep_recent_turns = 6 # authored bodies — wider; recoverable "read the file" marker once oldexempt_tools = [] # nothing exempt by default; add a tool to protect its input[strategies.stale_reads] — on in default, off in gentle
Section titled “[strategies.stale_reads] — on in default, off in gentle”Elides an old Read whose file was later superseded (re-read / Write / Edit on
the same path), and demand-pages the last read of a path once it exceeds
page_min_bytes. Never breaks a tool pair.
Recency gate (keep_recent_turns): a superseded Read is elided only once it
ages past the keep-recent window — never on the turn the model is using it. A read
the model re-reads or edits one or two turns later is still in the active working
set; eliding it immediately would force the model to re-issue the same Read
(re-billing the bytes and slowing the session). The same window gates both
superseded-elision and demand-paging, so trimwire only ever acts “some rounds
later.”
Hot-path guard: a path the model has Read more than once in the session
is never demand-paged. Paging the current view of a file the model keeps needing
would just force another re-read (paged out again next turn) — a read-spiral. So
demand-paging only pages genuinely one-shot large reads; the current view of a
file you keep re-reading is left verbatim. (This suppresses demand-paging only —
an earlier, superseded read of that same path is still elided once it ages past
keep_recent_turns.)
[strategies.stale_reads]enabled = truekeep_recent_turns = 4page_min_bytes = 16384 # default profile: only page reads larger than this (16 KB; lowered from 32 KB in v0.3.0). struct default 0 = offexempt_tools = []protected_file_patterns = [] # opt-in (default empty = off): globs of file paths that # are never superseded-elided OR demand-paged. Mirror the # same globs in [strategies.bloat_cap] for full protection.# stub = "[trimwire: stale read — superseded by a newer view later]"[strategies.thinking_strip] — on in BOTH profiles (struct default off)
Section titled “[strategies.thinking_strip] — on in BOTH profiles (struct default off)”Drops old thinking / redacted_thinking blocks (reasoning for
already-solved problems). Removals are replayed by reprune by signature, so the
cache still holds. default keeps the last 4 turns of reasoning, gentle the
last 8.
[strategies.thinking_strip]enabled = truekeep_recent_turns = 4[strategies.simhash_dedup] — OPT-IN (off in both profiles)
Section titled “[strategies.simhash_dedup] — OPT-IN (off in both profiles)”Catches near-duplicate tool_results that cross_turn_dedup (exact-match)
misses, using a SimHash within hamming_threshold bits. Off by default; enable
it if you have very repetitive tool output.
[strategies.simhash_dedup]enabled = falsekeep_recent_turns = 4 # struct default (simhash is opt-in; no profile sets it)hamming_threshold = 3 # ≤ this many differing bits (of 64) = duplicatemin_bytes = 512 # ignore results smaller than thisexempt_tools = []# stub = "[trimwire: near-duplicate…]"[strategies.system_shape_normalize] — OPT-IN (off by default)
Section titled “[strategies.system_shape_normalize] — OPT-IN (off by default)”Lifts a stray messages[0] whose role is "system" up into the top-level
system field. Some flows (after /compact, /clear, or a mid-session model
switch) can emit a leading system-role message, which the API rejects with a 400.
Off by default; enable it only if you actually hit that 400 (it mutates request
shape, so it stays opt-in to keep the default path transparent).
[strategies.system_shape_normalize]enabled = false[reprune] — stable-prefix re-pruning (on in both shipped profiles)
Section titled “[reprune] — stable-prefix re-pruning (on in both shipped profiles)”[reprune]enabled = true # on in both shipped profiles; set false to disablethreshold = 8 # re-prune cadence: new messages before a full re-prune (~2× keep_recent)recheckpoint_result_bytes = 131072 # default profile (128 KB); 0 = off (gentle + struct default). # Forces a re-checkpoint when the tail carries > this many bytes of # tool_result content, with no-advance-on-no-op rollback (the # byte-based re-checkpoint from the "Read coverage gap" fix).max_sessions = 1024ttl_secs = 3600By default trimwire re-prunes every request from scratch; as messages age out of
the recent window, the pruned prefix shifts turn-to-turn and busts Anthropic’s
prompt cache. With reprune on, trimwire keeps per-session state and, while the
conversation is append-only, re-uses the previous turn’s pruning decisions so
the pruned prefix stays byte-identical. It recomputes only once the tail grows
past threshold messages, or when history is rewritten (compaction). This cuts
prompt-cache churn and cost on long / churn-heavy sessions (offline-measured
−30–40%), at the cost of trimming the most-recent batch one checkpoint later.
Both shipped profiles (default and gentle) turn reprune on. That’s what keeps
the aggressive default cache-stable. Set [reprune] enabled = false to override.
It can never produce wrong output: any prefix change forces a full re-prune
identical to the stateless path (worst case is a cache miss). State is bounded by
max_sessions (LRU) and ttl_secs (idle eviction).
[summarizer] — optional model summary of OLD content (off by default)
Section titled “[summarizer] — optional model summary of OLD content (off by default)”Start with the wizard: trimwire summarizer setup writes this block for you.
The knobs below are for hand-tuning ~/.config/trimwire.toml afterward.
Opt-in. When engine is not "model-free" (and [reprune] is on), trimwire sends
the OLD prunable slice to a local ollama model or your own API key and replays
the resulting summary in place of that slice — a clean summary the model can read
instead of lossy elision markers. Requires reprune (it carries the summary across
turns). See SUMMARIZER.md for the full guide; this is the
knob reference. ToS: trimwire never originates calls on your Claude subscription
token — only on a local model or an API key you provide.
[summarizer]engine = "model-free" # "model-free" (off) | "local" | a provider id belowfallback = [] # ordered engine ids to try if the primary failsmode = "default" # "default" | "gentle" (a slower, more conservative cadence)trigger_bytes = 204800 # only engage once the request body exceeds this (200 KB)timeout_secs = 180 # hard timeout per summarizer callkeep_recent_turns = 6 # recent assistant turns never summarized (working set)resummarize_after_bytes = 32768 # re-summarize once this much new old-content accruesaccumulator = true # append frozen delta segments instead of replacingmax_summary_segments = 128 # frozen-segment cap (raise for very long sessions; cache-safe)# slice_char_budget = 131072 # max serialized bytes summarized per segment. Unset =# # per-engine default: local ~60 KB (num_ctx/OOM-safe, from# # the 25600 default below), API-only chain ~128 KB.# # Bigger = the summary owns more old content (API engines).accept_ratio = 1.0 # 1.0 = strict (keep summary only if smaller than model-# # free). >1.0 (e.g. 1.5, for strong API engines) keeps a# # higher-fidelity summary up to ratio× the model-free size# # (capped at +16 KB). Keep 1.0 for weak local models.
[summarizer.local] # used when engine/fallback includes "local"endpoint = "http://localhost:11434"model = "qwen3.5:4b" # an approved local tag; weaker tags are warned/refusedmax_num_ctx = 25600 # ollama num_ctx + local slice budget (≈max_num_ctx×2.5−2000# # chars ≈ 60 KB). KEEP THE DEFAULT for qwen3.5:4b: raising to# # 40000 (≈96 KB) FAILS it (N=10: 0/10, drops the same ~2 facts# # every run) — see MODEL-COMPATIBILITY.md. Only raise on a model# # you've gated at the larger size with `summarizer probe`.# # Clamped at 131072.keep_alive_secs = 0 # 0 = unload the model from RAM after each summarizer call# # (RAM-saving default). Raise (e.g. 60) to keep it warm# # between calls on a GPU/high-RAM box.
# [[summarizer.providers]] # one block per cloud provider; reference by `id`# id = "myapi"# style = "anthropic" # "anthropic" | "openai" (auth header + payload shape)# base_url = "https://api.example.com" # API root; style path /v1/... is appended# # (no trailing /v1 for openai)# full_url = "..." # OPTIONAL exact POST URL — bypasses base_url + /v1 for# # non-standard paths (Z.ai /paas/v4, Azure deployments).# # When set, base_url is ignored; style still applies.# model = "claude-haiku-4-5"# # Key source (pick one; trimwire stores the NAME/PATH, never the key):# api_key_file = "~/.myapi_key" # RECOMMENDED: read at runtime, so it works with the# # always-up service `trimwire install` sets up (a daemon# # can't see your shell's exported env vars). ~/ → $HOME; chmod 600.# api_key_env = "MYAPI_KEY" # OR an env-var name — fine for foreground `trimwire run`,# # invisible to a background service.See SUMMARIZER.md “Provider recipes” for copy-paste configs (OpenAI, OpenRouter, Anthropic, Z.ai anthropic + openai, Azure, self-hosted vLLM).
To let the summary own a large fraction of old content on a strong cloud model, use an
API-only chain (no "local" in engine/fallback) with accept_ratio = 1.5; the
128 KB API budget then covers far more per segment (raise slice_char_budget for more,
mindful of the provider’s context window). The deterministic strategies still run and
remain the full fallback when the summarizer is off or fails.
[ledger]
Section titled “[ledger]”Per-request savings + cache-prefix telemetry (byte counts and hashes only, no
message content). Pruned to retain_days at startup. Set enabled = false to
record nothing.
[ledger]enabled = truedb_path = "~/.trimwire/ledger.db"retain_days = 365Note db_path defaults to ~/.trimwire/, not an XDG_DATA_HOME location —
so setting XDG_DATA_HOME alone does not redirect the ledger. To point the
ledger somewhere else (e.g. to isolate a test run or manual canary from your real
data), set the env override TRIMWIRE_LEDGER__DB_PATH=/path/to/ledger.db.
[share] — opt-in anonymous telemetry (off by default)
Section titled “[share] — opt-in anonymous telemetry (off by default)”Only used by trimwire share stats and trimwire share benchmark.
Opt in with trimwire share enable (persists enabled = true); opt out with
trimwire share disable. Once enabled, trimwire share stats uploads without
--yes each run. --yes also works as a per-run override or first-time
confirmation. --force bypasses the once-per-day throttle.
The built-in community stats collector URL ships in the binary and points at
https://api.trimwire.dev/ingest. [share] endpoint exists as an override for
self-hosting or testing; it is not the normal path. The benchmark collector
ships the same way (built-in URL https://api.trimwire.dev/ingest-benchmark,
overridable via [share] benchmark_endpoint); trimwire share benchmark uploads
only with --yes, otherwise it dry-runs.
The payload is coarse, bucketed, and anonymized client-side; it never contains
prompts, code, paths, ids, IPs, or raw counts. See
TELEMETRY.md for the full field-by-field contract.
[share]enabled = false # set true via `trimwire share enable` to persist consentendpoint = "" # override for self-hosting/testing only; leave empty for the community collectorbenchmark_endpoint = "" # same, for `trimwire share benchmark`Seeing your savings
Section titled “Seeing your savings”trimwire stats always works (reads the ledger; no setup). For an in-session
view there are two pieces:
Live bar (the statusline) — opt-in
Section titled “Live bar (the statusline) — opt-in”trimwire install never touches your statusline. Adding the bar is a
separate, explicit step (Claude Code’s statusline is a single command with no
“field” API, so the bar can only live in statusLine):
trimwire statusline add # if you have NO statusline → makes trimwire your bartrimwire statusline wrap # if you ALREADY have one → adds a trimwire row beneath ittrimwire statusline remove # undo either (restores a wrapped original exactly)addwritesstatusLine → trimwire statusline renderto~/.claude/settings.json(atomic write; refuses if the file has JSON comments it can’t parse). If a statusline already exists it leaves it untouched and points you towrap.wrapmakes trimwire thestatusLine, runs your original bar (feeding it the same input), and prints a trimwire row beneath it. Your original command is stashed in~/.trimwire/statusline-wrapped.cmd;remove(anduninstall) restore it exactly.- (
trimwire statusline renderis the command those write into settings.json; Claude Code runs it. You never type it, so it’s hidden from--help.)
The bar prints ⊡ trimwire 128 KB (~32K tok) · 41% ↓ · 87 reqs for the current
session (· ready before any traffic; a yellow ⚠ not responding when it’s
set-but-down). It’s intentionally terser than an in-session AI assistant block (no
“messages/tools compressed” or topic; trimwire is a byte-level proxy). The full
per-strategy breakdown is in trimwire stats.
Heads-up on wrapping: Claude Code runs only one
statusLine, and the last tool to write that key wins it. Ifclaude-statuslineis reinstalled after you wrap, it’ll overwrite the key and trimwire’s row disappears. Just re-runtrimwire statusline wrap.
Manual integration into your own statusline script
Section titled “Manual integration into your own statusline script”If you’d rather embed trimwire inside your own script instead of wrapping (e.g.
claude-statusline, a single
shell script with no plugin hook), add trimwire as a segment. Both read
Claude Code’s JSON from stdin, and stdin can only be consumed once, so reuse
the $input your script already captured; don’t re-read stdin:
# in statusline.sh, right after the existing `input=$(cat)` line:tw_seg=$(printf '%s' "$input" | trimwire statusline render 2>/dev/null)
# then add "$tw_seg" to a row's join_segs call, e.g.:row2_right=$(join_segs "$cost_seg" "$tw_seg")join_segs skips empty segments, so when the ledger is empty or trimwire is
off the segment just vanishes. (If your justify helper isn’t ANSI-aware, the
colored segment may offset right-alignment. Drop the color by piping through
sed 's/\x1b\[[0-9;]*m//g' if so.)
Health alert hook (works without any statusline)
Section titled “Health alert hook (works without any statusline)”Wire trimwire hook as a SessionStart (and/or UserPromptSubmit) hook. It
stays silent when healthy and emits a visible systemMessage only when trimwire
is configured but not serving, so a silent “set but not pruning, paying full
price” failure can’t go unnoticed even if you don’t watch the statusline:
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "trimwire hook" } ] } ] } }Auto-report hook (opt-in, files a content-free issue on a detected anomaly)
Section titled “Auto-report hook (opt-in, files a content-free issue on a detected anomaly)”Wire trimwire report --auto as a Stop hook. At the end of each session it
checks that session for a trimwire anomaly (currently: an HTTP ≥400 response on a
turn where pruning fired — i.e. a prune that may have broken the request) and, if
found and not already filed, opens a content-free GitHub issue via gh
(versions + OS + a coarse cache-stability bucket only — no paths or session
content), deduped in <ledger-dir>/filed-issues. It is silent when clean,
never fails a session (a hung gh call is bounded to 15 s), and falls back to
printing a pre-filled report URL if gh isn’t available. Requires the gh CLI
authenticated with issue-write access.
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "trimwire report --auto 2>/dev/null || true" } ] } ] } }Run trimwire report (no --auto) any time to print a pre-filled issue URL you
submit yourself.
Runtime environment variables (not config-file keys)
Section titled “Runtime environment variables (not config-file keys)”These tune the running daemon, separate from the config layers above:
TRIMWIRE_AUDIT=<file>: opt-in wire audit. When set, the daemon appends one JSONL line per/v1/messagesdescribing the shape of each request (counts, sizes, flags, the model, theanthropic-betaheader, and the session id) and never any message content, tool input, or result text. It also records the cache-prefix structure (structural identifiers only): the ordered tool-definition names (tool_names), whichtools/systemblocks carry acache_controlbreakpoint, thesystemshape, and themessages[0]block-type sequence (first_msg_blocks) — used to investigate prompt-cache behaviour. Off by default (no cost when unset). Useful to see exactly what Claude Code sends on the wire and what trimwire forwards. Also available as--audit <file>ontrimwire serve/trimwire run.TRIMWIRE_LOG=info|debug|warn: gateway log verbosity (tracingfilter).
Tuning notes
Section titled “Tuning notes”The shipped defaults (threshold_bytes, keep_recent_turns, the
sliding_window denylist) are conservative starting points, not
telemetry-tuned values. Run a few real sessions, check trimwire stats, and
adjust. Per-profile savings/cost numbers and the methodology are in
benchmark/ (start with its headline tables).