Rust tool RTK trims agent terminal output to save tokens
Tools15 min read

Rust tool RTK trims agent terminal output to save tokens

Install the RTK hook in Claude Code, compare the same cargo test in two shapes, recover trimmed output, read the gain numbers and uninstall, step by step.

Say CI broke on a Friday afternoon and you asked a coding agent to “run the tests and find the cause” (a scene the editors made up). The agent runs cargo test and reads every line saying that 498 of 500 tests passed before it ever reaches the 2 that failed. The passing lines are useless for the diagnosis, yet they become input tokens, and that output is read again on later calls as part of the conversation history. RTK (Rust Token Killer) is a single executable that trims this output before the agent reads it. It is maintained by the GitHub organization rtk-ai; as of Oct 27, 2026 it has roughly 84,600 stars and 5,400 forks, and the latest stable release is v1.1.0 from Oct 25.

This article skips the product tour and follows the steps in order. You install it while avoiding a project that shares the name, register the hook, see how the cargo test from the scene above changes, pull up the original when the trimmed output is not enough, read the savings numbers, and finish by removing everything cleanly. The baseline is Claude Code; other agents are covered in a separate part midway.

Check the name before installing

Two projects use the name rtk: Rust Token Killer (rtk-ai/rtk), the subject of this article, and Rust Type Kit (reachingforthejack/rtk), which generates Rust types. The rtk on crates.io may point to the latter, so cargo install rtk can install the wrong tool. The official docs tell you to specify the Git URL when installing with Cargo.

There are five ways to install. The check steps below are the same whichever you pick.

# macOS / Linux (Homebrew tap, release binary)
brew install rtk-ai/tap/rtk
# Windows
winget install rtk-ai.rtk
# Cargo (always specify the Git URL)
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk

The README also lists brew install rtk. That one is the source-build formula in the official Homebrew repository, while rtk-ai/tap/rtk in the install guide is the formula that downloads the release binary. The install script (curl … | sh) and prebuilt binaries are official methods too. The script runs whatever it downloads without you reading it, so it is safer to save it to a file, skim it, and then run it, or to use a package manager. If you build from source yourself, you need Rust 1.91 or later.

When the install finishes, two commands tell you whether you got the right project.

rtk --version
rtk gain

If rtk gain shows the savings dashboard, it is the right tool. If you get the error rtk: 'gain' is not a rtk command, you installed Rust Type Kit; remove it with cargo uninstall rtk and reinstall using the Git URL above. If you installed with Cargo and see command not found: rtk, the usual cause is that ~/.cargo/bin is not on your PATH.

See which files change before enabling the hook

Installing rtk does not shrink the agent’s output by itself. You have to register a hook that steps in right before the agent runs a tool. Registering means editing configuration files, so it is best to follow the order the docs set out, with --dry-run first.

  1. See what will change. rtk init --global --dry-run lists the files it would create or edit as [dry-run] would … lines and writes nothing
  2. Connect it. rtk init --global installs the global Claude Code hook and registers it in ~/.claude/settings.json. An instruction file, RTK.md, is created along with it
  3. Restart the agent. The hook applies from newly started sessions
  4. Confirm the registration. Run rtk init --show to see whether the hook is registered, and grep rtk ~/.claude/settings.json to check that the settings file has an entry too
  5. Work as usual. After running a few commands, open rtk gain and rtk gain --history

If you want it for a single project only, run rtk init without --global. In that case no hook is installed; a note goes into CLAUDE.md and a .rtk/filters.toml template is created. With only a note and no hook, the agent is merely asked to use rtk, so for the trimming to really take effect the global hook is the right choice. On Windows, from v0.37.2 the hook is registered as a native binary command (rtk hook claude), so WSL is not needed. Other agents only change the option: rtk init -g --codex for Codex, rtk init -g --gemini for Gemini CLI, and rtk init -g --agent cursor for Cursor.

The same cargo test comes back in two shapes

Now back to the opening scene. Here is the cargo test output the model reads, without the hook and with it, side by side. Both screens are examples the editors made to match the filter behavior the docs describe; real output varies by project and version.

Terminal screen placing the full 500-test output without the hook next to the output with the hook, where only 2 failures and a recovery hint remain Figure 1. On the left, output that flows through including every passing test; on the right, the output the model actually reads (illustrative example)

The first line on the right is the pass/fail count, and below it only the failed tests and the key errors remain. The last line, [full output: rtk recall 36365b69eda6c4f1], is this tool’s safety net (the hash follows the format of an example in the official docs). When a command fails, or when a filter cuts a long list, RTK stores the full output in a local database and appends this hint to the end of the trimmed output. If the agent decides it needs the full output to find the cause, it runs rtk recall 36365b69eda6c4f1 exactly as the hint says, which costs one extra round trip.

The filters are designed to respect what the user asked for. The project’s design principle is to let more through when you explicitly ask for detail. cargo test shows only failures, but cargo test -- --nocapture keeps the whole output, and commands with detail options such as ls -la are treated in the same spirit. The docs also describe a safeguard in the code (never_worse) so that a filter result never ends up with more tokens than the original. You can approach the same goal by writing git status -sb, | tail -20 or --quiet into your instructions, but a person has to decide this per command, and it does nothing if the agent forgets the instruction. RTK does that work automatically in front of the command. The two approaches do not conflict, so you can use both.

The path one command takes through the hook

Let’s follow the route to see why the screen changed. In Claude Code the PreToolUse hook is what operates. Right before a tool runs, the hook receives the command string, looks it up in the rule table with rtk rewrite, and returns it rewritten as the matching rtk command. Neither the agent nor the user types the name rtk.

Diagram with two branches: the agent’s Bash call passes through the hook and rtk into short output, while the built-in Read, Grep and Glob skip the hook and the original goes into the model context Figure 2. The hook only catches Bash tool calls. The dotted line is the path when rtk is not found or errors, plus the original being saved

The green area at the top is the path the hook stands on. One dotted line is the path for when rtk cannot be found or a filter errors: the hook exits quietly and the original command runs as is. One of the project’s design principles is “RTK must not block a command’s execution or output.” The red area at the bottom is the part that is easiest to get confused about. Results from the agent’s built-in Read, Grep and Glob do not pass through the hook, so they are not trimmed. If you want them trimmed, have the agent use shell commands (cat, rg, find) or call rtk read, rtk grep and rtk find directly.

There are rules worth knowing about how the hook rewrites a command.

  • Compound commands. cargo fmt --all && cargo test 2>&1 | tail -20 is rewritten on both sides of &&, || and ;, becoming rtk cargo fmt --all and rtk cargo test. Middle stages of a pipe are left as they are, and the front part is rewritten only when the last stage or the command that follows is for display
  • Scripts inside quotes. bash -c "head foo && grep -R bar ." becomes bash -c "rtk read foo --head-lines 10 && rtk grep -R bar .". It handles the -c form of sh, bash, zsh and fish
  • Cases it leaves alone. Commands containing a heredoc (<<) or $((, commands that already start with rtk, and commands prefixed with RTK_DISABLED=1 are not rewritten
  • Permission rules. A command caught by a Claude Code deny rule is rejected, and one matching an ask rule requests confirmation. From v1.1.0, if the settings file cannot be read or parsed, it fails closed (blocks instead of allowing), and deny and ask rules are also applied to commands after variable assignments or wrappers

Savings differ by command

The README sums up what the filters do in four things: removing filler, grouping similar items, truncating length, and merging repeated log lines into counts. The development docs break this down into 12 strategies and give, for each, the reduction range the design aims for. Below, the strategies that the commands in this article belong to are placed on a single axis.

Chart showing each strategy’s design-target reduction as a range on a 0 to 100% axis: stats only 90 to 99%, failures only 94 to 99%, progress removal 85 to 95%, pattern grouping 80 to 90%, dedup 70 to 85%, tree 50 to 70%, minimal code reading mode 20 to 40% Figure 3. Design-target ranges from the development docs (not measurements)

What you notice first is that the ranges cluster toward the right. Git status and test results, whose output is structured and has only a few lines that matter, aim for 90% or more; ls, which has to keep its structure, is around half; and rtk read, which reads code, is the lowest at 20 to 40% even in minimal mode. A command with no filter passes through as it is and is recorded as 0%. These ranges are design targets, not values from your own sessions, so you check real numbers with rtk gain later. In the design docs’ git log example, about 500 characters (125 tokens) shrink to a 20-character (5-token) summary, a 96% reduction.

Here is how the representative commands in the README and official docs change.

Command Output the agent receives
ls / tree Tree form with file counts
git status Short summary grouped by status
git diff Diff with less surrounding context and no headers
git log Only hash, author and subject
git add/commit/push A one-line confirmation instead of progress output
cargo test / npm test Only the failures, with passing tests as a count
pytest Failed items and a shortened traceback
tsc / eslint Errors grouped into counts by rule and file
docker ps Only the essential columns such as name, image, status and ports
docker logs / kubectl logs Identical lines shown once with a count
curl Response body only, headers removed

How to read your first gain number

After using it for a few days, open rtk gain and you will see a Save% figure. This value means “how much the shell output bytes shrank.” The official docs draw the line several times: it is not the percentage by which your bill went down, only the shell output, which is a part of the input tokens, shrinks, and the model’s replies (output tokens), the prompt and the conversation history are untouched.

I calculated how the number gets diluted with two assumptions. Say a single call has 100,000 input tokens and the shell output shrinks by 90%. In a conversation-heavy session where shell output is 20,000 tokens of the input, the input becomes 82,000 tokens, an 18% reduction; in a test-and-build loop session where shell output is 60,000 tokens, it becomes 46,000 tokens, a 54% reduction.

Stacked bars: with 20K of shell output in 100K input and with 60K, after cutting 90% the total input is 82K (-18%) and 46K (-54%) Figure 4. The same 90% filter gives a total input reduction of 18% or 54%, depending on the session type (assumed values, editors’ calculation)

The same filter gives results that differ by nearly a factor of three. That means the more of the input the shell output makes up, the bigger the effect, and since the output tokens the model writes stay the same, the reduction in your bill is smaller still. So which side your own sessions lean toward is what matters.

Values you can trust

  • The Save% ratio. The same estimator is applied to the original and the result, so the ratio holds
  • The fact that commands without a filter are recorded as 0%
  • Long-term trends (--daily, --weekly, --monthly)

Values to treat as rough estimates

  • The token counts themselves. They are bytes divided by 4 and rounded up, with no tokenizer
  • The ratio against the subscription limit in --quota. It is stacked on the same rough estimate
  • Example values such as the 90% in the docs (they are sample output, not measurements)

The docs’ sample output says 50,000 input, 5,000 output and 45,000 saved (90.0%) over 10 commands, but it carries the caveat that these are “example values.” The README’s 60 to 90% is also the project’s own material, and within what the editors checked, no independent third-party measurement turned up. If you need an exact value, put the original and the filtered result through the model’s tokenizer and count them yourself.

There are also commands that check “is it taking effect” rather than giving numbers.

rtk gain --history      # last 10 commands and their savings
rtk discover            # find commands that passed without a filter, with estimates
rtk session             # share of recent-session commands that went through rtk
rtk -vvv git status     # show the filtering process in detail on stderr
rtk cc-economics        # view alongside cache write and read volumes

The docs say that if commands keep showing up in the rtk discover list after installation, it is a sign the hook is not active in that agent. When the adoption rate in rtk session is low, the cause is often that it was turned off with RTK_DISABLED=1, or that the hook is not active in a particular subagent. The official docs say you need not worry about the prompt cache. The filter runs once per command and fixes the output into the history, so the prefix the cache matches against does not change, and since a cache write costs 1.25 times the input price and a read 0.1 times, the smaller the result, the lighter it is both the first time and each time it is re-read per turn.

Check that it hooks your own agent

The README says 18 tools are supported, and the table in the agent support doc counts Copilot (VS Code and CLI) and Trae (project and global) separately, for 20 rows. The table matters because “supported” does not automatically mean “output shrinks.” It depends on what the agent’s hook API allows. Of the 20 rows, 16 allow command rewriting and 14 have rtk init install that hook, while 8 allow post-hoc output compression and 7 have it installed for them.

Agent What it can hook
Claude Code Rewriting, blocking, output compression, session-start hook and proxy routing are all possible. The widest range
Codex A rewrite applies only together with an explicit allow decision. Otherwise the hook fails and the original command runs
Gemini CLI Each time the input is modified, a “tool input was modified” system note is attached and a few tokens come back
Kilo Code, Cline, Windsurf (Devin), Kimi Code, Antigravity They use a rules file instead of a hook and merely ask the model to use rtk <command>. For now it does not block, rewrite or compress
Roo Code No hook. Only the proxy route exists
OpenCode The plugin is installed only on 1.18.29 or later, and rtk must be on the PATH the agent sees

With an agent that uses only a rules file, no rewriting happens even after rtk init finishes. This is where [awareness] level becomes important. The default, default, does not tell the agent about RTK, and RTK.md only explains “how to read trimmed output.” Raising it to high also explains rtk gain and the workarounds, and full tells the agent to prefix every command with rtk. An agent without a hook is always at full.

The docs also give a troubleshooting order for when the hook seems not to work. Confirm the install with rtk --version and rtk gain, run rtk init --global again and restart the agent, then check the registration with rtk init --show and the rtk entry in ~/.claude/settings.json. The repository also has scripts/check-installation.sh, which checks the install, paths and hook state in one go.

Escape hatches when the trimmed output is not enough

Filters reduce information, so a day will come when you need the part that was trimmed. The order to use then is as follows.

  • Recover it. rtk recall <hash> from the hint returns the original that was stored. When you look yourself, search with rtk recall <hash> --grep pattern and list entries with rtk recall --list. The store is built-in SQLite by default with lossless gzip compression, and the default retention limits are 10 MiB per entry, 200 entries (oldest deleted first) and 30 days. You change the mode with rtk config recall sqlite|tee|disabled
  • Raw just once. Prefix a command with rtk bypass <command> or something like RTK_DISABLED=1 git rebase main, and only that command skips the filter. To turn off recovery storage for a single run, prefix RTK_RECALL=0
  • Exclude entirely. Put commands where state matters into [hooks] exclude_commands = ["git rebase", "docker exec"] in the config and the hook leaves them alone. An entry that starts with ^ is a regular expression
  • Which filter is hiding things. rtk gain --recalls shows, per filter, “the share of trimmed output that the agent went back to retrieve.” A filter with a high share often hides information the agent needs, so it is a candidate for raising the limit or adding to the exclude list, while a low share means the cut part really was noise

You can also write your own filters for your own commands. Define them in .rtk/filters.toml (project) and ~/.config/rtk/filters.toml (global) with fields such as match_command, strip_lines_matching, keep_lines_matching, max_lines and on_empty, and verify them with input and output examples under [[tests…]]. Because a filter changes what the agent sees, applying a .rtk/filters.toml the moment you pull it from a repository could let someone else’s rules blind your agent. So RTK quietly skips that file, keyed on the SHA-256 of its contents, until you confirm it with rtk trust. The repository docs also state the limit: this is a device for consent and tamper evidence, not a sandbox, and an attacker who can write files with the same user privileges can edit the trust list too. When you open a repository you do not know, do not add --yes to rtk trust; read the contents.

Choosing an edition and the proxy

There are three things you can download, so tell them apart before installing. The install steps earlier were based on the first row (rtk).

What you download License What it contains
rtk Apache-2.0 + BUSL-1.1 The hook, all command filters, the compression engine
rtk-oss Apache-2.0 only The hook and command filters (no engine)
rtk-proxy BUSL-1.1 A separate daemon that compresses API traffic

rtk and rtk-oss install an executable with the same name, so they cannot coexist. Homebrew declares a conflict between the two formulas and blocks the second install, while the deb and rpm packages declare no conflict and stop at a file collision, so remove one before installing the other. You get rtk-oss with the install script plus RTK_EDITION=oss, or with brew install rtk-ai/tap/rtk-oss. Without the engine you lose rtk pipeline, rtk methods, rtk tasks and PostToolUse compression (the feature that, with the rtk:posthook extension enabled in pipeline.toml, also compresses the agent’s Read and MCP output), but the parts you use every day are the same: the rewrite hook, all command filters, rtk gain and rtk discover.

BUSL-1.1 is a source-available license: the source is public, but it does not meet the open source definition. According to the repository docs, use in production is free, while offering RTK or its derivatives to third parties as a commercial product or service is outside the permitted scope. If your company policy requires an OSI-approved license for every binary, pick rtk-oss. The rtk formula in the official Homebrew repository lists the license as Apache-2.0 while the formula in the tap lists Apache-2.0 and BUSL-1.1, so if policy is at stake, installing the one with the unambiguous edition name is the safe route.

The proxy works one level higher. If the hook shortens shell command output, rtk-proxy shortens the entire request the agent sends to the model (conversation history, tool results, file contents). It is a local HTTP daemon running on 127.0.0.1:8080; you point the agent’s ANTHROPIC_BASE_URL at the daemon, or start the daemon and route Claude Code through it with the single line rtk claude. It is an optional choice, independent of the hook. Because requests pass through this process, it widens the trust boundary by one more link, so turn it on knowing the scope the docs state. Prompt and response bodies are not stored; only CCR, an opt-in feature, keeps the compressed originals in rtk-proxy-memory.db for 7 days. Remote statistics are off by default, and only after you consent with rtk telemetry enable does one anonymous ping a day go out (version, OS, command count, top command names, savings rate); the docs state explicitly that command contents and file paths are not sent.

It is also good to know the release flow of the last ten days. Pre-releases such as dev-1.1.0-rc.574 are posted often in the release list, so ordinary users should stick to stable releases or the package manager version.

  1. v1.0.1 Fixed gain to point only to commands that exist, and made proxy start follow the bind setting
  2. dev-1.0.2-rc.567 Compresses repeated Ruff and SQLFluff explanations
  3. Pre-releases rc.568 to 573 Fish script wrapping (opt-in), improved compression of Angular build tables
  4. v1.1.0 Block when permission settings cannot be read, pass-through for help options, no git rewriting in Claude Code managed worktrees, integrity check of rewritten commands, keep pytest error lines

How to undo it

If you do not like it, remove it in the reverse order of installing. First strip out the hook and the instruction file.

rtk init -g --uninstall     # remove the hook, RTK.md and the settings.json entry

Then delete the executable according to how you installed it: brew uninstall rtk for Homebrew, winget uninstall rtk-ai.rtk for winget, and cargo uninstall rtk for Cargo. If you set up the proxy, the ANTHROPIC_BASE_URL that init wrote is removed too (when it points to the daemon address). The statistics records remain separately: on macOS, ~/Library/Application Support/rtk/history.db (kept 90 days by default) stays as it is, so check and reset it with rtk gain --reset or delete the file yourself. After removing the hook, restart the agent and run cargo test once to see that the output is back to its original shape, which closes the loop that began with the opening scene.

#RTK#token savings#Claude Code#coding agent#Rust#CLI