Shell output shaped at the source
The proxy digest folds a tool result after the agent has read it, and an agent that wants what was folded runs the command again. distil sh works one step earlier: it runs the command, applies one small deterministic filter chosen by what the command is, and prints the result. The compact form is the only one that ever enters the context. Opt-in; design and boundaries in ADR 0026.
$ distil sh -- python -m pytest -v tests/ # run one command shaped $ distil hook install --shell # Claude Code: rewrite Bash test runs to distil sh $ distil hook uninstall --shell $ distil hook --stats # receipts, by filter (sh:pytest, sh:git-status, …)
What it does to the output
- Always (lossless): ANSI colour codes stripped and carriage-return progress overwrites resolved, so the visible text is unchanged; exact repeated lines collapsed to
<<xN>>; forgit status, the advice lines about how to use git ((use "git add <file>..." …)) dropped. - Metered key, or opted in (elide): for test runners, the line printed for each passing test is dropped. Every failure, error, traceback, warning and the result summary stays verbatim, and one marker line says how many lines went and how to get them back:
FAILED tests/test_m.py::test_bad - AssertionError: boom =================== 1 failed, 31 passed in 0.02s =================== [distil sh sh-v1: 31 passing-test lines elided; failures, errors and the summary are verbatim above. Full output: `distil expand 24d14dad`]
The full output is written to distil's encrypted restore store and read back byte-exact before that line is printed; if it cannot be, you get the lossless form. distil expand <handle> | grep … recovers a slice.
Which commands
| Filter | Commands | Tier |
|---|---|---|
git-status | git status | lossless |
pytest | pytest, py.test, python -m pytest | elide |
unittest | python -m unittest, Django runtests.py | elide |
cargo-test · go-test | cargo test · go test | elide |
js-test | npm|yarn|pnpm test, jest, vitest, npx jest|vitest | elide |
Never rewritten: search and listing (grep, rg, find, ls), git diff/log/show, file reads. Search output is the agent's map of the code; in 1.56.3, digesting it cost extra steps on SWE-bench, so it stays verbatim. Any other command run through distil sh is executed untouched.
Subscriptions
The same rule as the post-tool hook: on a Claude Pro/Max login only the lossless tier runs, unless you install with --digest (distil hook install --shell --digest) or run distil sh --digest -- …. On a metered API key the elide tier is on. Installing the rewrite is itself an explicit command; nothing installs it for you.
Fail-open, and your permissions
- The Claude Code hook only prefixes
distil sh --onto one simple command (optionally aftercd DIR &&, optionally with2>&1and| tail/| head). Chains, redirects,$(…)and backticks are left exactly as written. - Any hook error,
distilmissing from PATH, orDISTIL_SH_OFF=1: the command runs as the agent wrote it.distil shruns a command it does not recognise untouched, and prints the raw output if a filter fails. The exit code is always the command's own. - Claude Code checks permission rules against the rewritten command, so distil never rewrites a command a
denyoraskrule could touch, and auto-approves only when one of your allow rules matches the original command. Otherwise you get Claude Code's normal prompt. Do not add a blanketBash(distil sh:*)allow rule: it would allow anything typed after it.
What it is worth, measured offline
On real tool output from synthetic projects (tests/fixtures/shell/), the lossless tier removes 15.9% of the tokens and the elide tier 61.3%. On the commands a SWE-bench Lite agent actually ran (300 tasks, 1,390 bash calls), only 27 calls match and 0.06% of bash-output tokens go: that workload is mostly grep and sed -n, which stay verbatim. So expect it to matter where agents run test suites, not on SWE-bench. It is opt-in until an outcome run shows it is non-inferior; the distil-sh arm of the SWE-bench harness exists to run that comparison. Numbers: benchmarks/results/2026-10-05/shell_replay*.json, regenerated by benchmarks/shell_replay.py.
Claude Code only for the automatic rewrite: Cursor's pre-shell hook can allow or deny but not rewrite, and Gemini CLI's and Codex CLI's were not verified to rewrite input. Any agent can be told to call distil sh -- itself.