Distill

July 25, 2026 · View on GitHub

AI coding agents burn most of their context window on output they never needed: 300 lines of passing-test dots to find 4 failures, a full git log to learn "what changed recently", a 60k-token diff to review one hunk. Distill compresses that output before the agent reads it — errors first, structure preserved, everything reversible.

Distill is a capability, not a sixth intelligence layer: it reuses the index the five layers already build (symbol bounds, graph centrality, hotspots) to decide what to keep, instead of compressing blind.

repowise distill pytest -x        # run pytest, print a compact errors-first rendering
repowise expand a1b2c3d4e5f6      # restore anything that was omitted
repowise saved                    # tokens & dollars saved so far

Guarantees (enforced by the engine, asserted by tests):

  • Errors always survive. Every error/failure-classified line in the raw output appears in the distilled rendering.
  • Fully reversible. Raw output is stored before any marker is emitted; repowise expand <ref> round-trips it byte-for-byte.
  • Fallback to raw. Any filter error, storage failure, or non-improvement prints the original output unchanged. Distillation can never lose output.
  • Net-positive only. Output is only distilled when it actually gets smaller (marker included) — small outputs pass through untouched.
  • Exit codes preserved. repowise distill <cmd> is a drop-in wrapper in scripts and agent tool calls alike.

The surfaces

1. repowise distill <cmd> — the executor

Runs the command (shell semantics preserved), captures stdout+stderr, picks a filter by command shape (then by content sniff), and prints the compact rendering. Eleven filters ship:

FilterCommandsWhat it keeps
test_outputpytest, jest, vitest, cargo test, go testfailures + assertion details + summary; collapses pass parades
build_outputnpm/tsc/cargo/go buildserrors and warnings grouped; strips progress/boilerplate
lint_outputeslint/biome, ruff/flake8/mypy, clippy, golangci-linterrors verbatim; warnings grouped by rule id with counts + file:line anchors; fixable totals
install_outputpip, uv, poetry, npm/pnpm/yarn install, cargo install, brew, bundle, composerwhat actually changed + the final summary; drops resolver and download progress. An all-satisfied run collapses to install: ok (no changes)
infra_planterraform/tofu plan, helm diff/upgradethe Plan: N to add… summary and the resources that change; drops unchanged-resource dumps. A no-op collapses to plan: no changes
git_statusgit statusporcelain-style compact status
git_loggit logrecent subjects + counts
git_diffgit diff/showstat + the most relevant hunks
search_resultsgrep / rg floodsgrouped-by-file digest with per-file counts and anchors
file_listingls / tree / findgrouped tree rendering
logsanything log-shapedtemplate-collapse with counts (timestamps/ids normalized)

Dropped content is stored in the omission store and referenced inline:

[repowise#a1b2c3d4e5f6: 230 lines omitted (~6.1k tokens); restore: repowise expand a1b2c3d4e5f6]

2. repowise expand <ref> — the reversal

repowise expand a1b2c3d4e5f6              # full original output
repowise expand a1b2c3d4e5f6 -q "FAILED"  # only the matching lines
repowise expand "[repowise#a1b2…]"        # a pasted whole marker works too

Looks in the current repo's store first, then the user-level fallback store. MCP clients without a shell can resolve the same refs through get_symbol("repowise#<ref>") — see MCP.

3. The command-rewrite hook (Claude Code + Codex)

A PreToolUse hook rewrites noisy agent commands — pytest -xrepowise distill pytest -x. The default posture is allow, so the rewrite runs without a prompt; it is a bounded substitution, never a new command (see Safety model). Set permission: ask in .repowise/config.yaml to review each rewrite instead. repowise init offers to install the hook (default: yes); install manually with:

repowise hook rewrite install      # or opt in during `repowise init`
repowise hook rewrite status
repowise hook rewrite uninstall    # removes only the repowise entries

The hook covers both Claude Code shell tools — Bash and, on Windows, PowerShell. Existing installs are widened automatically on the next install/init.

Codex. When ~/.codex exists, install also covers the Codex CLI, with two honest caveats its hook protocol imposes:

  • Codex applies a PreToolUse command rewrite only from version 0.137; on older builds the hook entry is skipped (a rewrite response would error on every shell call). repowise hook rewrite status reports what your build can do.
  • Codex has no ask-with-mutation — a rewrite can only be auto-allowed, never shown for approval. So under Codex, rewrites fire only for families resolving to permission: allow in .repowise/config.yaml, which the default posture already is; any family you set to ask passes through unchanged there rather than being silently escalated.

The hook entry lands in ~/.codex/hooks.json (one install covers every repo); Codex requires new hooks to be reviewed — run /hooks inside Codex to trust it. Independently of any hook, install maintains a marker-managed "Output Distillation" section in the repo's AGENTS.md teaching the agent to run repowise distill <cmd> voluntarily and to repowise expand markers instead of re-running commands — this works on every Codex version, including ones with no usable rewrite hook. uninstall removes the section and restores your AGENTS.md byte-for-byte.

The hook is deliberately conservative. It never rewrites:

  • redirections, compound commands (>, &&, ;, backticks, $()) and almost all pipes. Two safe shapes are carved out: a trailing 2>&1 (distill merges stderr into its capture anyway), and, on macOS/Linux only, a single pipe into a bare stdin filter — head, tail, grep, egrep, fgrep, rg — which runs unchanged inside distill's own shell (pytest -q | head -50repowise distill "pytest -q | head -50"). The grep/rg pattern-file forms (-f, --file) are excluded: they read a file as configuration rather than filtering stdin
  • watch/follow modes (--watch, tail -f, …)
  • anything on the trivial-command ignore-list, or already-prefixed commands
  • PowerShell-native constructs: Verb-Noun cmdlets, & "path" invocations, backtick continuations — and, from PowerShell, alias tokens (ls, cat, find, …) that don't mean what their unix namesakes mean
  • commands in repos that have not opted into repowise (no .repowise/ upward)

These decisions are made by tokenizing the command, not by scanning it for punctuation, so on macOS/Linux a shell character sitting inside quotes is treated as the text it is: pytest -k "a|b" is a normal test run, not a pipeline, and is rewritten as one. On Windows the blunter rule stands — any |, &, ;, <, > or backtick anywhere in the command, quoted or not, passes it through untouched — because cmd.exe re-parses metacharacters that the argv quoting Windows uses does not cover. The conservative answer always wins the ambiguity; an unrecognized shape is left alone.

The allowlist trap. A rewrite changes the command string, so a Claude Code permission rule you already had — say Bash(git diff:*) — no longer matches the rewritten repowise distill git diff …, and the permission prompt comes back for commands you had already allowed. The fix is one extra allow rule covering the distill prefix:

// ~/.claude/settings.json
"permissions": {
  "allow": [
    "Bash(repowise distill:*)",
    "PowerShell(repowise distill:*)"
  ]
}

repowise hook rewrite install offers to add these for you (or pass --allow-rule / --no-allow-rule to decide non-interactively). The default posture stays ask — the rule only stops double-asking for commands you've already vetted; repowise distill runs the wrapped command unchanged and never widens what it can do.

Per-repo behavior is configured under distill.commands in .repowise/config.yaml — see Configuration. Declining the repowise init prompt writes distill.commands.enabled: false, so a hook installed globally from another repo stays inert in this one. A multi-repo workspace init asks once and records the verdict in every selected repo; repowise hook rewrite install -w re-enables them all later. The hook answers in well under 100 ms (stdlib-only hot path, no database).

repowise init also adds a short "Output Distillation" section to the managed CLAUDE.md, teaching the agent to prefer repowise distill <cmd> voluntarily and to expand markers instead of re-running commands — this works in any agent that runs shell commands, hook or no hook.

4. Read intelligence — skeletons and stale-read notices

The index knows every symbol's line bounds, so repowise can render a file skeleton — every signature, imports, and the bodies of only the most central symbols — without parsing anything at query time:

get_context(["src/big_module.py"], include=["skeleton"])

A typical 600-line file skeletonizes to ~15% of its full tokens with every signature present. Body selection is importance-ranked (symbol PageRank, hotspot bit, query match) — this is where the index makes distillation smarter than blind truncation.

The existing PostToolUse hook complements this passively:

  • Skeleton nudge — after a large Read of an indexed file, the agent is told the skeleton's token cost vs the full file (once per file per session).
  • Stale-read notice — after an Edit/Write, a later Read of the same file warns that earlier excerpts predate the edit.
  • Search digest — grep floods (≥50 lines) get a compact grouped-by-file digest ordered by graph centrality.

5. MCP response budget — _meta.omitted

All MCP tool responses were always token-budgeted; before Distill, truncation was silent. Now every drop goes through the same omission store:

"_meta": {
  "omitted": {
    "refs": ["a1b2c3d4e5f6"],
    "tokens": 5840,
    "restore": "repowise expand <ref> (CLI) or get_symbol(\"repowise#<ref>\", query?) (MCP)"
  }
}

get_symbol resolves omission refs as well as symbol ids — the repowise#<12-hex> shape is unambiguous next to path/to/file.py::Name, and the optional query parameter searches within the stored content. Tool count stays at nine. See MCP_TOOLS.md.

Note: MCP tool calls now record a counterfactual saving — what raw file exploration the curated answer replaced — as mcp:<tool> rows in the same ledger (see repowise saved). Response truncation is folded into that delta (the delivered size is measured after the budget cap), so it is never double-counted.


The omission store

.repowise/omissions/omissions.db — a SQLite sidecar (WAL), deliberately separate from wiki.db so hook-time writes never contend with indexing. Falls back to ~/.repowise/omissions/ when the current directory is not inside a repowise repo.

  • Content is keyed by a 12-hex truncated SHA-256 — the same ref that appears in markers, so one store serves the CLI, the hook, and MCP.
  • Durable across sessions by design: an agent resuming work tomorrow can still expand yesterday's markers.
  • Pruning is TTL + size-cap based (7 days / 50 MB by default, configurable), applied opportunistically on write. The most recent row is never evicted, so a just-rendered marker cannot dangle.

repowise saved — the savings report

repowise saved                  # per-filter rollup + totals + est. dollars
repowise saved --by day         # daily rollup
repowise saved --by source      # cli vs hook-* vs mcp:<tool>
repowise saved --since 2026-06-01
repowise saved --model claude-opus-4-6   # price the estimate differently
repowise saved --missed                  # savings raw commands left on the table
repowise saved --missed --missed-days 30

Dollar estimates price saved tokens at the chosen model's input rate (saved tokens are input the agent never had to read) using the same pricing table as repowise costs. Token counts are chars/4 estimates.

The report covers both surfaces of the ledger: the distill command/hook path (cli / hook-* sources) and MCP counterfactual savings (mcp:<tool> sources — each tool answer priced against the raw exploration it replaced). repowise saved --by source separates them.

Missed savings — repowise saved --missed

The adoption feedback loop: how many tokens did raw commands waste that a filter would have caught? --missed scans your local Claude Code transcripts for Bash/PowerShell tool calls in this repo that were not routed through repowise distill, classifies each with the same router the engine uses, and estimates the foregone savings using each filter's conservative fixture floor (the per-filter minimums asserted in CI, not the medians — the estimate undersells on purpose). The scan covers the last 7 days by default (--missed-days N for more); plain repowise saved appends a one-line summary when there is anything to report.

The scan is read-only and best-effort: malformed or absent transcripts mean an empty report, never an error.

Privacy: the scan stays entirely local. Commands and outputs are read from your own transcript directory (~/.claude/projects/…) on this machine; nothing is uploaded, recorded, or sent anywhere. Codex transcripts are not yet scanned.

The local dashboard goes further. The Costs page leads with a savings hero card that combines two surfaces into one honest number:

  • Distill — the repowise distill command/hook ledger above.
  • MCP tool savings — what each tool answer replaced: the raw file exploration the agent would have done without it (source='mcp:*'). get_symbol stands in for reading the whole file, get_context for the files its skeletons summarise, search_codebase for opening each cited file; the estimate undersells. Tools without a counterfactual estimator still contribute their response-budget truncation drops.

The dollar figure is priced at the coding agent's actual model, not a flat guess: saved tokens are input the agent never read, so they are worth that agent's input rate. The dashboard detects the model from your local agent transcripts — the most-recent model that touched the repo across Claude Code (~/.claude/projects/…) and Codex (~/.codex/sessions/…) — and falls back to a sensible default when nothing is detectable. Detection is read-only and stays on this machine. The missed-savings scan rides along as an "unlock more" prompt.


repowise corrections — recurring command fumbles

The same transcript reader, pointed at a different waste: commands the agent got wrong and then fixed. The scan finds consecutive runs of the same base command where the first failed (the transcript records a real exit code) and a later variant succeeded, classifies the fumble, and aggregates the recurring rules:

repowise corrections                # report-only (default window: 30 days)
repowise corrections --days 60
repowise corrections --write        # maintain the managed guidance block
KindExample rule
wrong tooluse .venv\Scripts\python.exe instead of bare python
wrong pathpytest: use tests/unit/cli/, not ../../tests/unit/cli/
unknown flagpytest does not support --looponfail
missing argtool needs --required-thing

Classification is deliberately precision-first: apart from the structural wrong-tool case, a rule only forms when the error text corroborates it (the dropped flag or path is actually named by the error) — a red-green dev loop re-running tests with different selections never becomes a "correction". Wrong-path rules consult the symbol index when available and note where the corrected target actually lives.

--write (strictly opt-in) maintains a short "Known command corrections" managed block — most-frequent rules first, at least 2 occurrences each, capped at 10 lines — between REPOWISE_CORRECTIONS markers in the repo's .claude/CLAUDE.md (and AGENTS.md when one exists), so the next agent session is told up front. Re-running --write refreshes the block in place; when no rule clears the threshold anymore, the block is removed. Content outside the markers is never touched.

The same privacy contract as the missed scan applies: read-only, best-effort, entirely local.


Measured savings

On a public OSS repository (microdot), one run per command, tokens estimated chars/4 — the same estimator the ledger uses:

CommandRaw tokensDistilledSaved
pytest -q (11 failures)3,3741,31761% — all 11 FAILED lines preserved
git log -503,06433189%
git diff (30 commits of history)62,8338,63586%
git log --oneline -303213210% — already compact, passed through
git status (clean tree)83830% — too small to distill, passed through

The 0% rows are the net-positive guard working: distill never bloats small output. In an end-to-end agent spot-check on the same repo (a seeded 11-failure bug), the agent diagnosed the exact root-cause line and fix from the distilled test output — identical conclusion to the raw-output run.

Fixture-suite medians across the core filters: ≥60% reduction on test/build/lint output with zero error-line loss (asserted in CI).

Install and infra-plan logs compress harder, because they are mostly repeated boilerplate. Measured on real captures from this repository, one run each:

CommandSaved
npm ci (1,344 packages)99.5%
pip install (everything already satisfied)collapses to install: ok (no changes)
terraform plan80.2%

Every one of those still round-trips through repowise expand, and the error line in a failing install survives verbatim.


Configuration

The distill: block in .repowise/config.yaml:

distill:
  enabled: true                  # master switch for this repo
  commands:
    enabled: true                # the command path (CLI + hook rewrites)
    permission: allow            # ask | allow | off — hook posture (default allow)
    families:                    # per-filter overrides
      test_output: allow         # auto-allow rewrites for test runs
      git_diff: deny             # never rewrite git diff here
    disabled_filters: []         # filters to skip entirely, e.g. [logs]
  omission_store:
    ttl_days: 7                  # prune stored omissions after this
    max_mb: 50                   # size cap, oldest pruned first

Everything defaults sensibly with no block present. repowise doctor validates the block (unknown keys, bad permission values, unknown filter names, non-positive store sizing) and reports the store size against its cap and whether the rewrite hook is installed.


Safety model, in one place

RiskMitigation
A filter eats a critical lineerrors-first invariant + fixture tests + expand recovery + fallback-to-raw
Silent permission escalationrewrites default to allow, which is not an escalation: a rewrite is always repowise distill <one recognized command> drawn from a closed family set, never an arbitrary command smuggled behind the wrapper. Set permission: ask to approve each one. Codex has no ask primitive, so only families explicitly set to allow rewrite there
Marker with nothing behind itcontent stored before the marker renders; store failure ⇒ raw output
Compound-command semanticscompound commands, substitution and redirects are never rewritten. The one pipe shape that is (a single stage into a bare stdin filter, macOS/Linux only) is passed through as one quoted token and runs verbatim in distill's shell; anything that could break out of that quoting bails
Unindexed or stale repofilters work index-free; index only improves ranking
Store growthTTL + size cap, pruned on write; repowise doctor reports size

See also