rtk (Rust Token Killer) is a Rust CLI that wraps common dev commands — git, grep, cargo test, cat — and compresses their output before it reaches an LLM's context window. The premise is simple: most command output is boilerplate that the model doesn't need. The git commit message doesn't require six lines. Forty-five passing tests don't need to be listed. The implementation is entirely heuristic — no ML, no LLM, just Rust transforming output strings.

I cloned the repo, built it from source, and ran every example against the rtk codebase itself. All numbers below are real. The project is currently at v0.24.0 and, in a nice piece of circularity, its own commit history is full of Co-authored-by: Claude Sonnet 4.6 lines — it's built with the very tool it optimises for.

The hook is the clever part

rtk's primary integration target is Claude Code. Running rtk init --global installs a bash script at ~/.claude/hooks/rtk-rewrite.sh and registers it as a Claude Code PreToolUse:Bash hook. Every time Claude Code tries to execute a shell command, this hook fires first.

The hook reads the tool call JSON from stdin, extracts the command string, and pattern-matches against a long list of known commands. If it matches — git status, cargo test, rg, cat, and twenty others — it rewrites the command in place and outputs the modified JSON. Claude Code executes the rewritten command and sees compressed output. It never knows the interception happened.

# excerpt from rtk-rewrite.sh
if echo "$MATCH_CMD" | grep -qE '^git[[:space:]]'; then
  case "$GIT_SUBCMD" in
    status|diff|log|add|commit|push|pull|...)
      REWRITTEN="${ENV_PREFIX}rtk $CMD_BODY"
      ;;
  esac
fi

# output the rewrite instruction back to Claude Code
jq -n --argjson updated "$UPDATED_INPUT" '{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": $updated
  }
}'

Commands not in the list pass through unchanged. The hook is zero-overhead for anything it doesn't recognise.

git log: 83% reduction

Standard git log produces a 6-line block per commit: full 40-char hash, Author line, Date line, blank, message body, blank. For a commit with a multi-paragraph body the block can run to 30+ lines. Here are the last five commits on the rtk repo:

$ git log -5

commit dd643d2322ac91472d4ee2de67ec284f0524a7fc
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date:   Wed Mar 4 12:48:19 2026 +0100

    chore(master): release 0.24.0 (#302)

    Co-authored-by: github-actions[bot] <...>

commit 4eb6cf4b1a2333cb710970e40a96f1004d4ab0fa
Author: USerik <serik.uatkhanov@gmail.com>
Date:   Wed Mar 4 16:14:11 2026 +0500

    fix(playwright): fix JSON parser to match real Playwright output format (#193)

    Three bugs caused `rtk playwright test` to always fail with EXIT: 1:

    1. **Wrong argument order**: `--reporter=json` was inserted before the
       subcommand (e.g. `playwright --reporter=json test`), but Playwright
       requires flags after the subcommand (`playwright test --reporter=json`).
    ...
    [30 more lines]

commit b934466364c131de2656eefabe933965f8424e18
...

rtk forces --pretty=format:%h %s (%ar) <%an>, caps at 10 commits, and truncates messages at 80 chars:

$ rtk git log -5

dd643d2 chore(master): release 0.24.0 (#302) (18 hours ago) <github-actions[b...
4eb6cf4 fix(playwright): fix JSON parser to match real Playwright output form...
b934466 feat: add AWS CLI and psql modules with token-optimized output (#216)...
772b501 feat: passthrough fallback when Clap parse fails + review fixes (#200...
a413e57 missing bracket (2 days ago) <aesoft>

~900 tokens to ~150. The LLM gets the shape of recent history without reading every word of every commit message.

git show: 96% reduction on big commits

This is where the numbers get dramatic. Commit b934466 added aws_cmd.rs (880 lines) and psql_cmd.rs (382 lines). Raw git show dumps the entire diff — 1,433 lines, roughly 14,000 tokens. rtk produces:

$ rtk git show b934466

b934466 feat: add AWS CLI and psql modules with token-optimized output (#216) (20 hours ago) <Itai>
.claude/hooks/rtk-rewrite.sh |   8 +
 hooks/rtk-rewrite.sh         |   8 +
 src/aws_cmd.rs               | 880 +++++++++++++++++++++++++++
 src/main.rs                  |  26 ++
 src/psql_cmd.rs              | 382 +++++++++++++++++++
 src/utils.rs                 |  34 ++
 6 files changed, 1338 insertions(+)

📄 src/aws_cmd.rs
  @@ -0,0 +1,880 @@
  +//! AWS CLI output compression.
  +//!
  +//! Replaces verbose `--output table`/`text` with JSON, then compresses.
  +//! Specialized filters for high-frequency commands (STS, S3, EC2, ECS, RDS, CloudFormation).
  +
  +use crate::json_cmd;
  +use crate::tracking;
  ... (truncated)
  +880 -0

📄 src/psql_cmd.rs
  @@ -0,0 +1,382 @@
  +//! PostgreSQL client (psql) output compression.
  ...
  +382 -0

The approach: one-line summary first, then --stat, then a custom compact_diff() that caps each hunk at 10 changed lines and emits ... (truncated). ~14,000 tokens to ~525. The model understands what changed without reading 880 lines of new Rust.

git status: 66% reduction

The raw output includes parenthetical instructions nobody needs after their first week of using git:

$ git status

On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   src/newfeature.rs

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/git.rs
$ rtk git status

📌 master...origin/master
✅ Staged: 1 files
   src/newfeature.rs
📝 Modified: 1 files
   src/git.rs

rtk uses git status --porcelain=v2 --branch internally and rebuilds the display from the structured output. The instructional noise — (use "git restore...") repeated twice — is the first thing to go.

grep: 71% reduction

Raw ripgrep output is a flat list: full paths, full lines, no grouping. Searching for three function names across the rtk source:

$ grep -rn "compact_diff\|filter_log_output\|run_status" src/

src/git.rs:42:        GitCommand::Status => run_status(args, verbose, global_args),
src/git.rs:133:        let compacted = compact_diff(&diff_stdout, max_lines.unwrap_or(100));
src/git.rs:253:        let compacted = compact_diff(diff_text, max_lines.unwrap_or(100));
src/git.rs:273:pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String {
src/git.rs:407:    let filtered = filter_log_output(&stdout, limit);
src/git.rs:421:fn filter_log_output(output: &str, limit: usize) -> String {
src/git.rs:572:fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
src/git.rs:1169:                let compacted = compact_diff(&stdout, 100);
src/git.rs:1443:    fn test_compact_diff() {
src/git.rs:1452:        let result = compact_diff(diff, 100);
src/git.rs:1585:    fn test_filter_log_output() {
... [11 more lines]
src/discover/report.rs:6:    /// Dedicated handler with filtering ...
src/gh_cmd.rs:1061:        let compacted = git::compact_diff(&raw, 100);
$ rtk grep "compact_diff\|filter_log_output\|run_status" src/

🔍 22 in 3F:

📄 src/discover/report.rs (1):
     6: /// Dedicated handler with filtering (e.g., git status → git.rs:run_status())

📄 src/gh_cmd.rs (1):
  1061: let compacted = git::compact_diff(&raw, 100);

📄 src/git.rs (20):
    42: GitCommand::Status => run_status(args, verbose, global_args),
   133: let compacted = compact_diff(&diff_stdout, max_lines.unwrap_or(100));
   253: let compacted = compact_diff(diff_text, max_lines.unwrap_or(100));
   273: pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String {
   407: let filtered = filter_log_output(&stdout, limit);
   421: fn filter_log_output(output: &str, limit: usize) -> String {
   572: fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Resul...
  1169: let compacted = compact_diff(&stdout, 100);
  1443: fn test_compact_diff() {
  1452: let result = compact_diff(diff, 100);
  +10

Results are grouped by file, line content is trimmed and centred on the match, long paths are abbreviated (src/.../git.rs), and a count summary leads. The structure makes it faster to scan — which matters for a model processing it serially.

rtk err: 77% reduction on build output

A Rust build that emits 18 "Compiling X v1.0..." lines before hitting two errors. Raw output is 49 lines. rtk err <command> runs it, then regex-filters to lines matching error|warn|failed|exception|panic and their indented continuations:

$ rtk err "cargo build"

error[E0308]: mismatched types
  --> src/main.rs:42:19
   |
error[E0425]: cannot find value `undefined_var` in this scope
  --> src/main.rs:67:15
   |
warning: unused variable: `result`
  --> src/lib.rs:12:9
   |
   = note: `#[warn(unused_variables)]` on by default
error: aborting due to 2 previous errors

The 18 compiling lines disappear. The model gets the two errors, their source locations, and nothing else.

read --level aggressive: 71% reduction

This is the most interesting mode. rtk read src/git.rs -l aggressive strips all function bodies and keeps only signatures, imports, and declarations. A 1,781-line file becomes 346 lines:

use crate::tracking;
use anyhow::{Context, Result};
use std::ffi::OsString;
use std::process::Command;
pub enum GitCommand {
    // ... implementation
fn git_cmd(global_args: &[String]) -> Command {
    // ... implementation
pub fn run(
    // ... implementation
fn run_diff(
    // ... implementation
pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String {
    // ... implementation
fn run_log(
    // ... implementation
fn filter_log_output(output: &str, limit: usize) -> String {
    // ... implementation
fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> {
    // ... implementation

The implementation uses a brace-depth tracker: once a function signature is found, it counts { and } characters and drops everything until depth returns to zero. The result is a complete public API surface — useful when an agent needs to orient itself in a file without reading every cmd.arg(arg); line.

The minimal filter, by contrast, only strips comments and normalises blank lines. On code-heavy files like this one it barely moves the needle — 6% in my test. It's more useful on files with dense documentation blocks.

The numbers, summarised

CommandRaw tokensRTK tokensReduction
git log -5~900~15083%
git show (1,338-line commit)~14,000~52596%
git status (dirty tree)~70~2466%
grep (3 patterns, 3 files, 22 hits)~240~7071%
rtk err (build, 18 compile lines)~450~10577%
read aggressive (1,781-line file)~14,100~4,05071%

Token counts use rtk's own heuristic: ceil(chars / 4). It's a rough approximation — short tokens like {, fn, if each cost one token regardless, so real tokenizer output would differ. The savings dashboard (rtk gain) is tracking this heuristic, not actual tokenizer counts. The relative comparisons are still meaningful; the absolute numbers are not.

What it doesn't do

rtk has no semantic understanding. rtk json strips all values from a JSON structure, leaving only the schema — useful for orientation but useless if the agent actually needs the InstanceId or IP address. git add and git push collapse to ok ✓ abc1234, which is great until something goes wrong and you've lost the error output. The tee mechanism writes full output to a log file and hints the path, but the agent has to know to look there.

The hook interception is also Claude Code-specific. The underlying compression logic — the test failure extractor, the diff compaction, the aggressive file filter — is applicable anywhere, but you have to plumb it in yourself for other harnesses.

The core insight

Everything here is hand-written heuristics. Regex patterns for error lines. Hard-coded format strings for git log. Brace-depth tracking for function bodies. No ML, no model calls, no adaptation.

That's actually a strength. The savings are predictable, fast, and offline. And the premise holds up: the bulk of what passes through an LLM agent's shell tool in a normal coding session genuinely is redundant. Git status doesn't need to explain what git restore does. A successful test run doesn't need 47 lines of test names. A commit message doesn't need its full hash.

Whether this is worth adding to your Claude Code setup depends on how much time your agent spends running repetitive shell commands on large repos. On small projects, the overhead of the hook (a jq parse per command) probably washes out the gain. On large ones, particularly with heavy git diff and test output, the reduction in context churn is real.