Anatomy of a token filter: RTK under the microscope

TL;DR. Chapter 26 drove RTK as a user; Chapter 27 benchmarked it. This chapter opens it up. We read the actual v0.43.0 source (Apache-2.0, about 20,000 lines of Rust) and replay every stage live on this machine: the PreToolUse hook that swaps a command mid-flight with a 16-line JSON reply, the registry that rewrites 82 command families and refuses anything it cannot attest, the two-stage git log filter that starts saving tokens before the command even runs, the never_worse guard, the ceil(chars/4) ledger in SQLite, and the tee escrow that keeps every dropped byte recoverable. The dissection also surfaces something you can only see at this level: RTK's own meter underreports its savings, because the biggest lever (argument injection) fires before the measurement starts. On our lab repo, git log really shrinks 597 tokens to 228 (a 62 percent cut) while the ledger records 17.4 percent. The chapter ends with the five design rules worth stealing for any filter you build yourself.

Contents

Why dissect a filter at all

Because "how does it know the LLM will read this?" is the question that separates people who use context tools from people who understand them. RTK advertises itself as a token-saving command proxy, and Chapter 27 confirmed the headline on this machine's own traffic. But a benchmark treats the tool as a black box. This chapter treats it as a white box, for two reasons.

First, RTK is the cleanest available specimen of a whole species: the tool-result filter, a program that sits at the exact choke point where command output becomes model context. Everything it does (rewrite, shape, cut, guard, meter, escrow) is something any filter at that choke point must decide about, including one you write yourself in an afternoon.

Second, the source is public and small enough to actually read. The Homebrew formula points at the repository, so we can fetch the exact code that produced the binary on this machine:

brew cat rtk | head -6
class Rtk < Formula
  desc "CLI proxy to minimize LLM token consumption"
  homepage "https://www.rtk-ai.app/"
  url "https://github.com/rtk-ai/rtk/archive/refs/tags/v0.43.0.tar.gz"
  sha256 "196bec9e9b438f0b8cd0198f68e05f072ccdfdec2c2655a3562d6ea357fa485b"
  license "Apache-2.0"

Every source excerpt below is copied verbatim from that v0.43.0 tarball, and every command output is real, captured on this machine. Where line numbers matter we cite the file so you can follow along in your own checkout.

One prompt, end to end

Start with the guarantee, because everything else hangs off it. Suppose you ask the agent:

"What changed in the last three commits?"

The model answers by emitting a tool_use block, the only way it can touch your machine (Chapter 17 walked this loop):

{"name": "Bash", "input": {"command": "git log --stat -n 3"}}

Claude Code is about to execute that command, but the user's settings.json registers a PreToolUse hook (Chapter 28 covered the hook channel):

"hooks": {
  "PreToolUse": [
    { "matcher": "Bash",
      "hooks": [ { "type": "command", "command": "rtk hook claude" } ] }
  ]
}

So before execution, the harness pipes the tool call to rtk hook claude on stdin. We can impersonate the harness with printf and watch the exchange:

printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git log --stat -n 3"}}' \
  | rtk hook claude
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecisionReason":"RTK auto-rewrite","updatedInput":{"command":"rtk git log --stat -n 3"}}}

That updatedInput field is the whole trick. The harness replaces the model's command with the rewritten one, executes rtk git log --stat -n 3, and whatever that prints to stdout becomes the tool_result block appended to the conversation. The Claude API is stateless: on every turn the harness resends the entire message history, tool results included, as input tokens (Chapter 23 dissected exactly which usage bucket they land in). There is no side channel where output could go instead. So the answer to "how is consumption guaranteed?" is: it is not detected, it is structural. The hook installed itself at the one point in the loop where every byte is already destined for the context window.

Don't be confused. The hook rewrite is not a shell alias or a PATH shim. An alias changes what your terminal runs; it cannot see the agent's tool calls, which never pass through your shell profile. The hook operates on the tool call itself, inside the harness, before a shell is even spawned. Your interactive git log is untouched.

The rest of this chapter follows those tokens through RTK's insides: what decides the rewrite, what shapes the output, what guards the result, and what the meter writes down.

Stage 0: the hook, sixteen lines that swap your command

rtk hook claude reads at most 1 MiB of stdin (STDIN_CAP in src/hooks/hook_cmd.rs), parses it as JSON, and pattern-matches the tool name. The detection is deliberately narrow:

#![allow(unused)]
fn main() {
// src/hooks/hook_cmd.rs
fn detect_format(v: &Value) -> HookFormat {
    // VS Code Copilot Chat / Claude Code: snake_case keys
    if let Some(tool_name) = v.get("tool_name").and_then(|t| t.as_str()) {
        if matches!(tool_name, "runTerminalCommand" | "Bash" | "bash") {
            if let Some(cmd) = v
                .pointer("/tool_input/command")
                .and_then(|c| c.as_str())
                .filter(|c| !c.is_empty())
            {
                return HookFormat::VsCode { command: cmd.to_string() };
            }
        }
        return HookFormat::PassThrough;
    }
    ...
}

Anything that is not a shell command passes through in silence. We can prove the two silent paths live. A Read tool call produces no output at all, so the harness proceeds unmodified:

printf '%s' '{"tool_name":"Read","tool_input":{"file_path":"/etc/hosts"}}' | rtk hook claude

And so does a Bash command containing a command substitution:

printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git log $(date +%F)"}}' | rtk hook claude

That second silence is a security decision, and we will come back to it in the next section.

When the hook does rewrite, one more subtlety hides in the reply. Compare the JSON we captured above with the source that builds it:

#![allow(unused)]
fn main() {
// src/hooks/hook_cmd.rs, process_claude_payload()
let mut hook_output = json!({
    "hookEventName": PRE_TOOL_USE_KEY,
    "permissionDecisionReason": "RTK auto-rewrite",
    "updatedInput": updated_input
});

if allow {
    hook_output
        .as_object_mut()
        .unwrap()
        .insert("permissionDecision".into(), json!("allow"));
}
}

permissionDecision: "allow" is inserted only when allow is true, and allow comes from checking the command against the user's own Claude Code permission rules (src/hooks/permissions.rs reads ~/.claude/settings.json, the project's .claude/settings.json, and settings.local.json, in that order). Our captured reply had no permissionDecision field: this box has no allow rule for git log, so RTK rewrote the command but stayed silent on permissions, leaving Claude Code's normal prompt flow in charge of the rewritten command. A hook that blanket-allowed everything it rewrote would be quietly escalating its own privileges. This one refuses to.

The rewrite registry, and when it refuses

The decision "does this command have an RTK equivalent?" lives in src/discover/registry.rs, a table of 82 command families, each mapping a raw command pattern to its rtk equivalent plus a category and an estimated savings percentage. The unit tests show the shape of an entry:

#![allow(unused)]
fn main() {
// src/discover/registry.rs (test)
assert_eq!(
    classify_command("git status"),
    Classification::Supported {
        rtk_equivalent: "rtk git",
        category: "Git",
        estimated_savings_pct: 70.0,
        status: RtkStatus::Existing,
    }
);
}

You can call the classifier yourself through rtk rewrite, which prints the rewritten command and reports its verdict in the exit code. The doc comment in src/hooks/rewrite_cmd.rs is the contract:

#![allow(unused)]
fn main() {
/// | Exit | Stdout   | Meaning                                                      |
/// |------|----------|--------------------------------------------------------------|
/// | 0    | rewritten| Rewrite allowed — hook may auto-allow the rewritten command. |
/// | 1    | (none)   | No RTK equivalent — hook passes through unchanged.           |
/// | 2    | (none)   | Deny rule matched — hook defers to Claude Code native deny.  |
/// | 3    | rewritten| Ask rule matched — hook rewrites but lets Claude Code prompt.|
}

Live, on this machine:

rtk rewrite "git log --stat -n 3"; echo " <- exit $?"
rtk rewrite "htop"; echo "(no output) <- exit $?"
rtk git log --stat -n 3 <- exit 3
(no output) <- exit 1

Exit 3, not 0, for the same reason the hook reply omitted permissionDecision: no allow rule matches here, so the rewrite happens but permission stays with the harness. htop has no entry in the registry, so it passes through untouched.

Before the registry is even consulted, the command must survive a screening for constructs the rewriter cannot reason about. This is the source of the silent treatment our git log $(date +%F) received:

#![allow(unused)]
fn main() {
// src/discover/lexer.rs
pub fn contains_unattestable_construct(cmd: &str) -> bool {
    if contains_substitution(cmd) {
        return true;
    }
    let tokens = tokenize(cmd);
    tokens
        .iter()
        .enumerate()
        .any(|(i, tok)| tok.kind == TokenKind::Redirect && redirect_has_file_target(&tokens, i))
}
}

Command substitution ($(...) or backticks), process substitution, and redirects into files all disqualify a command from rewriting, and the lexer is quote-aware: bash expands $(...) inside double quotes but not single quotes, and the lexer knows the difference. The reasoning is worth internalizing. A rewriter that touches git log $(deploy.sh) might change when or whether the substitution runs; a filter that intercepts foo > out.txt would capture bytes the user meant for a file. When RTK cannot attest that the rewritten command is behaviorally identical, it declines to rewrite at all. The registry also refuses heredocs and arithmetic expansion, and it normalizes backslash-newline line continuations before matching, closing a bypass where a leading continuation defeated the matcher (the comment in registry.rs cites the project's issue #1564). Compound commands get split on &&, ||, ;, and |, and each segment is rewritten independently, which is why a chain like git status && git log becomes rtk git status && rtk git log.

Stage 1: argument injection, filtering before the command runs

Here is the part that surprised us, and that no amount of black-box benchmarking reveals. RTK's git log filter does most of its work before git produces a single byte, by rebuilding the argument list. From src/cmds/git/git.rs:

#![allow(unused)]
fn main() {
// src/cmds/git/git.rs, run_log()
// Apply RTK defaults only if user didn't specify them
// Use %b (body) to preserve first line of commit body for agent context
if !has_format_flag {
    cmd.args(["--pretty=format:%h %s (%ar) <%an>%n%b%n---END---"]);
}

// Determine limit: respect user's explicit -N flag, use sensible defaults otherwise
let (limit, user_set_limit) = if has_limit_flag {
    let n = parse_user_limit(args).unwrap_or(10);
    (n, true)
} else if has_format_flag {
    cmd.arg("-50");
    (50, false)
} else {
    cmd.arg("-10");
    (10, false)
};
}

Unless you asked for a specific format, RTK swaps git's default four-line-header, full-body presentation for a one-line header (hash subject (relative date) <author>) followed by the body and an ---END--- sentinel that stage 2 will use to find block boundaries. Unless you asked for a count, it caps history at 10 commits, and it appends --no-merges. Note the politeness protocol running through the whole function: every injection checks whether the user (or the model) already expressed an opinion, and defers if so. A model that deliberately runs git log -30 --pretty=fuller gets exactly that.

This is context engineering's cheapest trick, and it is worth a box because it generalizes far beyond RTK.

Don't be confused. Filtering output and asking for less output are different levers. Most CLI tools have flags that shape their own output (--stat vs -p, --oneline, --porcelain, -n); pulling those flags costs nothing and loses nothing that was requested. Post-hoc filtering can only delete what already got generated. RTK pulls the flag lever first and the deletion lever second, and as we will see, its own meter only watches the second.

Stage 2: the post-filter

What git prints then flows through filter_log_output, which splits on the injected ---END--- sentinel and compresses each commit block:

#![allow(unused)]
fn main() {
// src/cmds/git/git.rs, filter_log_output()
// Remaining lines are the body — keep up to 3 non-empty, non-trailer lines
let all_body_lines: Vec<&str> = lines
    .map(|l| l.trim())
    .filter(|l| {
        !l.is_empty()
            && !l.starts_with("Signed-off-by:")
            && !l.starts_with("Co-authored-by:")
    })
    .collect();
}

Headers get truncated to 80 columns (120 if you set your own -n, on the theory that an explicit count signals you want more detail per commit), bodies keep at most three non-empty lines, and Signed-off-by: / Co-authored-by: trailers are dropped entirely: ceremony a model never needs to see. The first line of a commit body survives on purpose. The comment in run_log explains why: bodies carry BREAKING CHANGE notes and design rationale, exactly the lines an agent asked to summarize history actually wants.

The lab: watching both stages on a real repo

Reading code tells you intent; running it tells you truth. We build a disposable repo whose history exercises every rule we just read: 13 commits (three past the cap), one commit with a long subject, a multi-line body, and both trailer types.

mkdir rtk-lab && cd rtk-lab && git init -q filter-lab && cd filter-lab
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
  echo "line $i" >> app.py
  git add app.py
  git commit -q -m "feat: add feature $i to the parser module"
done
echo notes > notes.md && git add notes.md
git commit -q -m "fix: stop the tokenizer from splitting emoji into surrogate halves" -m "The old code indexed by UTF-16 code unit, so any astral-plane character
was cut in half and produced two replacement glyphs downstream.

This switches the whole pipeline to char indices.

Signed-off-by: Ada <ada@lab.dev>
Co-authored-by: Grace <grace@lab.dev>"

(Your hashes and relative dates will differ; everything else reproduces.) Raw git first. The newest commit alone spends 14 lines before history even starts:

git log > raw.txt
head -14 raw.txt
commit 29b02ab1d6c6e745b4e8e3987fff4d279544943b
Author: Ada <ada@lab.dev>
Date:   Thu Jul 2 23:32:16 2026 -0400

    fix: stop the tokenizer from splitting emoji into surrogate halves
    
    The old code indexed by UTF-16 code unit, so any astral-plane character
    was cut in half and produced two replacement glyphs downstream.
    
    This switches the whole pipeline to char indices.
    
    Signed-off-by: Ada <ada@lab.dev>
    Co-authored-by: Grace <grace@lab.dev>

Now the filtered version:

rtk git log > filtered.txt
cat filtered.txt
29b02ab fix: stop the tokenizer from splitting emoji into surrogate halves (0...
  The old code indexed by UTF-16 code unit, so any astral-plane character
  was cut in half and produced two replacement glyphs downstream.
  This switches the whole pipeline to char indices.
ce2eaf5 feat: add feature 12 to the parser module (0 seconds ago) <Ada>
b836d16 feat: add feature 11 to the parser module (0 seconds ago) <Ada>
caadde4 feat: add feature 10 to the parser module (0 seconds ago) <Ada>
879920b feat: add feature 9 to the parser module (0 seconds ago) <Ada>
441dc8d feat: add feature 8 to the parser module (0 seconds ago) <Ada>
3c4f6e8 feat: add feature 7 to the parser module (0 seconds ago) <Ada>
eac4c13 feat: add feature 6 to the parser module (1 second ago) <Ada>
bdaa746 feat: add feature 5 to the parser module (1 second ago) <Ada>
3ad3d29 feat: add feature 4 to the parser module (1 second ago) <Ada>

Every rule from the source is visible in the output. Thirteen commits became ten (the injected -10). The long header hit the 80-column knife mid-parenthesis ((0...). The body kept its three content lines. Both trailers vanished. And features 1 through 3 are simply gone, which is the honest cost of the cap: stage 1 is lossy, and Chapter 27's central lesson applies to it (a ratio without a loss inspection is meaningless). The byte count:

wc -c raw.txt filtered.txt
    2387 raw.txt
     912 filtered.txt
    3299 total

A 62 percent cut on this history. The --stat variant from our end-to-end prompt compresses less dramatically (939 to 577 bytes) because file-change tables are already dense, which matches the per-command spread Chapter 27 measured: the wordier the raw output, the bigger RTK's bite.

The safety nets: never_worse and the tee escrow

Two mechanisms keep the filtering from ever becoming a liability, and both are small enough to quote whole. The first guards the size direction. Every filtered result passes through src/core/guard.rs on its way out:

#![allow(unused)]
fn main() {
//! Never-worse output guard: RTK never emits more tokens than the raw command.

use crate::core::tracking::estimate_tokens;

/// Returns `filtered`, or `raw` when `filtered` would emit more tokens.
pub fn never_worse<'a>(raw: &'a str, filtered: &'a str) -> &'a str {
    if estimate_tokens(filtered) > estimate_tokens(raw) {
        raw
    } else {
        filtered
    }
}
}

If a filter's decorations (match counts, headers, escrow pointers) ever cost more than they save, the raw output ships instead. A filter with this guard can be wrong but never counterproductive.

The second net guards the information direction. When a filter drops a lot of content, the full output is teed to disk first (src/core/tee.rs: outputs under 500 bytes skip the escrow, files rotate at 20, each capped at 1 MiB). The grep filter shows both the cut and the receipt. Generate 120 matching lines:

python3 -c "
lines = ['def handler_%03d(): return parse(payload_%03d)' % (i,i) for i in range(120)]
open('handlers.py','w').write('\n'.join(lines)+'\n')"
rtk grep parse handlers.py
120 matches in 1 files:

1:def handler_000(): return parse(payload_000)
2:def handler_001(): return parse(payload_001)
3:def handler_002(): return parse(payload_002)
4:def handler_003(): return parse(payload_003)
5:def handler_004(): return parse(payload_004)
6:def handler_005(): return parse(payload_005)
7:def handler_006(): return parse(payload_006)
8:def handler_007(): return parse(payload_007)
9:def handler_008(): return parse(payload_008)
10:def handler_009(): return parse(payload_009)
11:def handler_010(): return parse(payload_010)
12:def handler_011(): return parse(payload_011)
13:def handler_012(): return parse(payload_012)
14:def handler_013(): return parse(payload_013)
15:def handler_014(): return parse(payload_014)
16:def handler_015(): return parse(payload_015)
17:def handler_016(): return parse(payload_016)
18:def handler_017(): return parse(payload_017)
19:def handler_018(): return parse(payload_018)
20:def handler_019(): return parse(payload_019)
21:def handler_020(): return parse(payload_020)
22:def handler_021(): return parse(payload_021)
23:def handler_022(): return parse(payload_022)
24:def handler_023(): return parse(payload_023)
25:def handler_024(): return parse(payload_024)
  +95 more in handlers.py [see remaining: tail -n +26 ~/Library/Application Support/rtk/tee/1783049536_grep_0_handlers_py.log]

The last line is the design insight: after truncating, the filter tells the model where the rest lives, as a command the model can run. If the agent decides match 87 matters, it recovers it with one cheap tail instead of re-running the search or, worse, guessing. Compression with a receipt beats compression with amnesia. During the research for this chapter, RTK filtered one of our own source greps mid-investigation and handed back exactly such a pointer; we followed it and lost nothing.

The ledger: ceil(chars/4), and what the meter cannot see

Every filtered command ends with timer.track(original, rtk_cmd, &result.stdout, &filtered), which writes one row to a SQLite database. The token arithmetic behind every number rtk gain ever shows you is four lines:

#![allow(unused)]
fn main() {
// src/core/tracking.rs
pub fn estimate_tokens(text: &str) -> usize {
    // ~4 chars per token on average
    (text.len() as f64 / 4.0).ceil() as usize
}
}

No tokenizer, no API call: ceil(chars/4), the same rule of thumb Chapter 2 introduced. The doc comment is upfront that this is a tracking approximation. The database lives at ~/Library/Application Support/rtk/history.db on macOS (~/.local/share/rtk/ on Linux), and its schema is one table wide enough to hold the whole story:

sqlite3 -header -column ~/Library/"Application Support"/rtk/history.db \
  "SELECT original_cmd, input_tokens, output_tokens, saved_tokens,
          ROUND(savings_pct,1) AS pct
   FROM commands ORDER BY id DESC LIMIT 2;"
original_cmd         input_tokens  output_tokens  saved_tokens  pct 
-------------------  ------------  -------------  ------------  ----
git log --stat -n 3  169           144            25            14.8
git log              276           228            48            17.4

Those are our two lab commands. And here is the discovery. Our raw.txt was 2,387 chars, which is ceil(2387/4) = 597 tokens, yet the ledger recorded input_tokens = 276. The meter never saw vanilla git log at all. Look again at run_log: timer.track receives result.stdout, the output of git with the injected format and cap already applied. We can reproduce that intermediate stream by running RTK's actual git invocation ourselves:

git log --pretty='format:%h %s (%ar) <%an>%n%b%n---END---' -10 --no-merges > shaped.txt
python3 -c "
import math
for name in ['raw.txt','shaped.txt','filtered.txt']:
    n = len(open(name,'rb').read())
    print(f'{name}: {n} chars -> ceil(n/4) = {math.ceil(n/4)} tokens')
"
raw.txt: 2387 chars -> ceil(n/4) = 597 tokens
shaped.txt: 1104 chars -> ceil(n/4) = 276 tokens
filtered.txt: 912 chars -> ceil(n/4) = 228 tokens

The chain reconciles to the exact token: 276 is the ledger's input_tokens, 228 its output_tokens. The pipeline was 597 → 276 → 228, but the meter only watches the second arrow. Stage 1's 321-token cut, the majority of the whole saving, is invisible to RTK's own analytics. The true end-to-end reduction on this command is 62 percent; the ledger says 17.4.

So the meter is honest about what it measures and blind to what it prevents, and the blindness points in the conservative direction: whatever rtk gain claims, the input-shaping wins come on top. This is the mirror image of Chapter 27's Headroom lesson. There, a 98 percent ratio overstated usefulness because nobody inspected the loss. Here, a 17 percent ratio understates it because the measurement starts after the biggest lever has fired. Both are the same moral: a meter is a claim about a pipeline stage, not about the pipeline, and you cannot know which stage without reading the code or testing the boundary yourself.

Don't be confused. The ledger's input_tokens and the API usage block's input_tokens (Chapter 23) share a name and nothing else. RTK's is the estimated size of the command output entering the filter; the API's counts everything entering the model. The two meet only in the sense that whatever RTK emits eventually becomes part of the API's input count on the next turn.

How a row is born

The gathering mechanics are worth spelling out, because every number in every report descends from one call at the end of every filter. Each filter starts a stopwatch (tracking::TimedExecution::start(), which just records an Instant) before running the underlying command, and ends with the timer.track(...) call we saw in run_log. That lands in Tracker::record, which computes the whole row locally:

#![allow(unused)]
fn main() {
// src/core/tracking.rs, Tracker::record()
let saved = input_tokens.saturating_sub(output_tokens);
let pct = if input_tokens > 0 {
    (saved as f64 / input_tokens as f64) * 100.0
} else {
    0.0
};

let project_path = current_project_path_string(); // added: record cwd

self.conn.execute(
    "INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms)
     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
    ...
}

Nine columns, all computed at insert time: an RFC 3339 UTC timestamp, the original and rewritten command strings, the canonicalized working directory (current_project_path_string() is just std::env::current_dir() canonicalized), the two token estimates, their difference, the percentage, and the stopwatch's milliseconds. exec_time_ms is what rtk gain averages into its "avg 7ms" line, and it answers the reasonable worry that a filtering proxy adds latency: the stopwatch wraps the whole execution, underlying command included, so the filter overhead is bounded by it. Note what is absent: no raw output text, no filtered text, no file paths beyond the cwd. The ledger stores measurements, not content, which is why it is safe to leave lying around in your home directory.

Failures get their own quieter channel. If a filter's parser chokes on unexpected output, RTK falls back to passing the raw output through (so the agent is never blocked), and logs the event to a second table via record_parse_failure_silent, a function whose entire error handling is "never crash over bookkeeping":

#![allow(unused)]
fn main() {
// src/core/tracking.rs
conn.execute(
    "CREATE TABLE IF NOT EXISTS parse_failures (
        id INTEGER PRIMARY KEY,
        timestamp TEXT NOT NULL,
        raw_command TEXT NOT NULL,
        error_message TEXT NOT NULL,
        fallback_succeeded INTEGER NOT NULL DEFAULT 0
    )",
    [],
)?;
}

Where the ledger lives, and when it dies

Storage is a single SQLite file named by HISTORY_DB in src/core/constants.rs, placed under the platform data directory: ~/Library/Application Support/rtk/history.db on macOS, ~/.local/share/rtk/history.db on Linux, %APPDATA%\rtk\history.db on Windows. Rows are not kept forever. Every insert ends with a cleanup_old() sweep that deletes anything older than DEFAULT_HISTORY_DAYS = 90 from both tables, so the ledger is a rolling three-month window that maintains itself: no cron job, no vacuum ritual, the write path is the retention policy.

From rows to the dashboard

rtk gain is then just SQL over that one table. The summary is a SUM and a ratio (note that the "percent saved" it prints is SUM(saved)/SUM(input), a token-weighted average, not the mean of per-command percentages, so one huge diff dominates a hundred tiny ls calls). The per-command table is a GROUP BY on the command string, and the project scoping uses that project_path column: rtk gain alone reports global scope, while inside a repo it can filter rows to the current directory subtree (the query matches exact path or path/* with SQL GLOB, avoiding LIKE because _ and % are legal in file paths and would act as wildcards). Live, mid-chapter, on this machine:

rtk gain --history | head -9
RTK Token Savings (Global Scope)
════════════════════════════════════════════════════════════

Total commands:    146
Input tokens:      651.3K
Output tokens:     404.9K
Tokens saved:      246.3K (37.8%)
Total exec time:   24.8s (avg 170ms)
Efficiency meter: █████████░░░░░░░░░░░░░░░ 37.8%

Those 146 commands include this chapter's own lab runs; the instrument measures itself being used. And now you can read every field with source-level precision: "Input tokens" is SUM(input_tokens), the post-injection stdout estimates, ceil(chars/4) each; "saved" is the stage-2 delta only; and the whole thing forgets rows older than 90 days.

The long tail: 63 filters in TOML

The hand-written Rust filters cover the high-traffic commands (git alone is a 3,400-line file, and gh, cargo, pytest, docker, and friends get the same treatment). The long tail is declarative: v0.43.0 ships 63 filters as TOML files under src/filters/, compiled into the binary and interpreted by a generic engine (src/core/toml_filter.rs). Here is df.toml, complete with its regression tests:

[filters.df]
description = "Compact df output — truncate wide columns, limit rows"
match_command = "^df(\\s|$)"
strip_ansi = true
truncate_lines_at = 80
max_lines = 20

[[tests.df]]
name = "short output passes through unchanged"
input = "Filesystem     1K-blocks   Used Available Use% Mounted on\n/dev/sda1        4096000 123456   3972544   4% /"
expected = "Filesystem     1K-blocks   Used Available Use% Mounted on\n/dev/sda1        4096000 123456   3972544   4% /"

A filter is a match pattern plus a pipeline of primitives (strip ANSI, truncate lines, cap lines, strip or keep by pattern), with test cases riding in the same file. The engine also reads a user file at ~/Library/Application Support/rtk/filters.toml, so extending RTK to a tool it has never heard of requires no Rust at all: the marginal cost of covering one more command is a dozen lines of configuration. That economy is why the registry can afford 82 entries.

What to steal for your own filters

Strip away the Rust and RTK is five design rules. They apply to anything you put between a command and a context window, including a 30-line wrapper script.

  1. Sit at the choke point, not beside it. Rewriting the tool call inside the harness is what makes consumption structural rather than hopeful. A tool the model must remember to invoke saves tokens only when the model remembers (Chapter 20 measured how often that fails).
  2. Ask for less before you delete. The flag lever (--pretty, -n, --porcelain) is free and reversible; the deletion lever is lossy. Pull them in that order, and defer whenever the caller already chose.
  3. Guard the floor. A never_worse comparison costs one length check and converts your worst case from "made things worse" to "did nothing."
  4. Escrow, don't destroy. Drop bytes from the context, not from the world, and tell the model where the rest lives so recovery costs one command, not one re-run.
  5. Know what your meter measures. RTK's ledger is exact about stage 2 and blind to stage 1. That is fine, because it errs conservative and the source says so. Whatever you instrument, write down which arrow of your pipeline the number describes, or your future self will quote it for the wrong one.

And one rule about trust rather than tokens: refuse what you cannot attest. The unattestable list (substitutions, redirects, heredocs) is RTK deciding that correctness outranks savings. A filter that rewrites everything eventually rewrites something whose behavior it changed.

Takeaways

  • The consumption guarantee is structural: a PreToolUse hook rewrites the model's own tool call, and the rewritten command's stdout is the tool result the stateless API loop resends every turn. Nothing is detected; there is nowhere else for the bytes to go.
  • The hook is narrow and polite: 1 MiB stdin cap, exact tool-name match, silence for anything it will not touch, and permissionDecision: "allow" only when the user's own rules already allow the command.
  • Filtering happens twice: argument injection before the command runs (format strings, caps, --no-merges) and block-level post-filtering after (80-column headers, 3-line bodies, trailer stripping). On the lab repo the pair cut git log from 597 tokens to 228.
  • Two safety nets make the filtering trustworthy: never_worse guarantees the filter never emits more than raw, and the tee escrow keeps dropped bytes on disk behind a pointer the model can follow.
  • The ledger is ceil(chars/4) into SQLite, and it measures only the post-filter stage: the ledger said 17.4 percent on a command whose true cut was 62. Meters describe pipeline stages, not pipelines; this one at least errs conservative.
  • Stats are gathered by one timer.track() call per filtered command, stored as nine computed columns (no content, just measurements plus the cwd for project scoping) in history.db under the platform data dir, self-pruned to a 90-day window on every insert, with parse failures logged to their own table. rtk gain is plain SQL over that file, and its percentage is token-weighted, not a mean of per-command ratios.
  • 82 rewrite families, 63 of them declarative TOML with embedded tests, plus a user extension file: the marginal filter costs configuration, not code.

👉 That is the microscope pass: one tool, opened to the byte level, and five rules worth carrying out of it. But five rules extracted from one specimen might just be that specimen's habits. Next we put a second, independently written filter under the same lens, snip, and find out which parts of the anatomy are RTK and which parts are the species.