compression with a quality contract

Post-tool hooks for agents a proxy cannot reach

Some agents let you set a base URL and some do not. A post-tool hook needs none: the client hands distil a tool result the moment it is produced, and uses what distil hands back instead. Same digest as the proxy, the original kept on this machine, and nothing touches your credentials.

$ distil setup --hooks                 # every client found on this machine
$ distil hook install --client gemini  # or one: claude | cursor | gemini | codex | all
$ distil hook status
$ distil hook --selftest               # the adapters, proven offline

Which tier runs: the proxy's billing policy

distil hook status shows, per client, which tier is active and why.

What the hook does

Tier-0 first (JSON minified, exact repeats collapsed), then — when the tier above allows it — for verbose line output the same decision-aware Tier-1 digest the proxy uses: head, tail, every error and verdict line kept, the quiet middle folded behind a handle. A digest is emitted only after the full original is written to distil's encrypted restore store and read back byte-exact; the stub ends with the command that recovers it:

<< +412 lines, handle=3f9a1c2e >>
=== 12 passed in 3.2s ===
<< full output: `distil expand 3f9a1c2e` >>

The agent can run that in its own shell, or call distil_expand if the distil MCP server is connected. Output under 2 KB is left alone, and anything that would not come out smaller in tokens is left alone.

What it never touches

Per client

Each contract was read from the client's own documentation on the date shown. A client whose hook cannot replace a result is listed as unsupported, not approximated.

ClientEvent → how the result is replacedWhat is compressedConfig writtenSource
Claude CodePostToolUse → hookSpecificOutput.updatedToolOutputBash, MCP tools~/.claude/settings.jsondocs (2026-09-25)
CursorpostToolUse → updated_mcp_tool_outputMCP tools only: the docs scope the override to MCP, and afterShellExecution is observe-only~/.cursor/hooks.jsondocs (2026-09-25)
Gemini CLIAfterTool → decision: "deny" + reason, which "replaces the tool result sent back to the model"run_shell_command, MCP tools~/.gemini/settings.jsondocs (2026-09-25)
Codex CLIPostToolUse → decision: "block" + reason: Codex "replaces the tool result with that feedback"Bash, all-text MCP results$CODEX_HOME/hooks.json (default ~/.codex)docs (2026-09-25)
WindsurfUnsupported. post_run_command and post_mcp_tool_use are observe-only: only pre-hooks can act (exit code 2 blocks), no output field replaces a result, and post_run_command does not receive the command's output.docs (2026-09-25)
Two things to know. Gemini CLI and Codex deliver a replacement through their block/deny channel, so distil opens it with one line saying the output was compacted, not refused. Codex runs a new or changed hook only after you trust it: open /hooks in Codex once after installing. (Codex documents updatedMCPToolOutput as parsed but unsupported and marks the hook failed, so distil never emits it.)

How the config is written

Atomically: a 0600 temp file, fsynced, renamed over the original with its mode and owner kept; a symlinked config is written through, not replaced. Unreadable JSON, or a hooks entry in a shape distil does not recognise, is refused rather than overwritten. What distil created — the file, the hooks object, the event list — is recorded only after the write succeeds, and uninstall removes only distil's entry and the containers distil itself created. Your other hooks are never touched.

What it saved

distil hook --stats reads the hook's content-free receipts (client, tool, characters before and after). --selftest writes none.