RTK: The CLI Proxy That Cuts Your AI Coding Token Bill by 80%

RTK: The CLI Proxy That Cuts Your AI Coding Token Bill by 80%

中文 EN

If you're using Claude Code, Cursor, Copilot, or any AI coding agent, you're burning tokens on noise — verbose git output, full test logs, directory listings padded with permissions and timestamps. RTK (Rust Token Killer) is a single Rust binary that sits between your AI agent and the shell, filtering and compressing command output before it ever hits the LLM context window.


Why This Matters

A typical 30-minute Claude Code session consumes ~118,000 tokens on shell command output alone. With RTK, that drops to ~24,000 — an 80% reduction. That's real money saved on API calls, and more importantly, it means your agent has more context budget for actual reasoning instead of parsing git push boilerplate.

CommandWithout RTKWith RTKSavings
ls / tree (x10)2,000400-80%
cat / read (x20)40,00012,000-70%
grep / rg (x8)16,0003,200-80%
git status/log/diff (x20)15,5003,600-77%
Test runners (x12)39,0003,900-90%
Total~118,000~23,900-80%

How It Works

Without RTK:
  Claude  --git status-->  shell  -->  git
    ^                                   |
    |         ~2,000 tokens (raw)       |
    +-----------------------------------+

With RTK:
  Claude  --git status-->  RTK  -->  git
    ^                       |          |
    |   ~200 tokens         | filter   |
    +---- (filtered) -------+----------+

RTK applies four strategies to compress output:

  1. Smart Filtering — Strips noise: comments, whitespace, boilerplate. A git push that normally spits out 15 lines of progress bars becomes just ok main.

  2. Grouping — Aggregates similar items. Test results get collapsed to "FAILED: 2/15 tests" with only the failing test details. Lint errors get grouped by rule or file.

  3. Truncation — Keeps the relevant head and tail, cuts the middle. Long file reads preserve signatures and key sections.

  4. Deduplication — Collapses repeated log lines with counts. Docker logs that repeat the same health check 500 times become one line with a count.

The magic is in the auto-rewrite hook: once installed, RTK transparently intercepts every Bash tool call your AI agent makes. git status gets rewritten to rtk git status before execution. The AI agent never knows — it just gets cleaner, shorter output.


Installation & Setup

Install

# Homebrew (recommended)
brew install rtk

# Or quick install script
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh

# Or via Cargo
cargo install --git https://github.com/rtk-ai/rtk

# Verify
rtk --version

Configure for Your AI Tool

# Claude Code (default) — one command, done
rtk init -g

# Other tools:
rtk init -g --gemini        # Gemini CLI
rtk init -g --codex         # OpenAI Codex
rtk init -g --agent cursor  # Cursor
rtk init --agent windsurf   # Windsurf
rtk init --agent cline      # Cline / Roo Code

Then restart your AI tool. That's it. Every shell command your agent runs now goes through RTK automatically.


Daily Usage

You Don't Need to Change Anything

After rtk init -g, the hook handles everything transparently. Your AI agent calls git status, it actually runs rtk git status, and gets compressed output. Zero workflow changes.

Direct Commands (when you want explicit control)

# File operations
rtk ls .                        # Compact directory tree
rtk read file.rs                # Smart file reading
rtk read file.rs -l aggressive  # Signatures only (strips function bodies)
rtk grep "pattern" .            # Grouped search results

# Git — verbose output becomes one-liners
rtk git status                  # Compact status
rtk git log -n 10               # One-line per commit
rtk git push                    # -> "ok main"
rtk git commit -m "msg"         # -> "ok abc1234"

# Tests — only failures shown
rtk cargo test                  # -90% tokens
rtk pytest                      # -90% tokens
rtk vitest run                  # Failures only

# Build & lint
rtk tsc                         # TypeScript errors grouped by file
rtk cargo clippy                # -80%
rtk ruff check                  # -80%

Important Caveat

The hook only intercepts Bash tool calls. Claude Code's built-in tools (Read, Grep, Glob) bypass the hook entirely. If you want RTK filtering for those workflows, use the shell equivalents:

  • Instead of Read -> use rtk read file.rs
  • Instead of Grep -> use rtk grep "pattern" .
  • Instead of Glob -> use rtk find "*.rs" .

Track Your Savings

rtk gain                # Summary stats
rtk gain --graph        # ASCII graph (last 30 days)
rtk gain --daily        # Day-by-day breakdown
rtk discover            # Find missed savings opportunities
rtk session             # Show RTK adoption across recent sessions

Configuration

Config file: ~/Library/Application Support/rtk/config.toml (macOS)

[hooks]
exclude_commands = ["curl", "playwright"]  # Skip rewrite for these

[tee]
enabled = true          # Save raw output on failure
mode = "failures"       # "failures", "always", or "never"

The tee feature is clever: when a command fails, RTK saves the full unfiltered output to disk so the LLM can read it without re-executing. You get the compressed version first, and the full version is available if needed.


Supported AI Tools (12 and counting)

ToolCommandMethod
Claude Codertk init -gPreToolUse hook
GitHub Copilotrtk init -g --copilotPreToolUse hook
Cursorrtk init -g --agent cursorhooks.json
Gemini CLIrtk init -g --geminiBeforeTool hook
OpenAI Codexrtk init -g --codexAGENTS.md injection
Windsurfrtk init --agent windsurf.windsurfrules
Cline / Roo Codertk init --agent cline.clinerules
Kilo Codertk init --agent kilocoderules file
Google Antigravityrtk init --agent antigravityrules file

Deep Dive: How RTK Works Internally

Architecture

RTK is a single Rust binary with a modular design:

src/
├── main.rs          # CLI entry point, Clap argument parsing
├── hooks/           # Hook system (Claude Code, Cursor, Gemini, etc.)
│   ├── init.rs      # rtk init — installs hooks for each AI tool
│   ├── rewrite_cmd.rs  # Command rewriting logic
│   ├── integrity.rs    # SHA-256 hook pinning (tamper detection)
│   └── trust.rs        # Trust model for project-local filters
├── filters/         # 60+ TOML-defined filter rules
│   ├── cargo.toml, pytest.toml, git.toml, docker.toml, ...
│   └── (each defines match patterns + output transformations)
├── cmds/            # Built-in command handlers (ls, read, grep, git, etc.)
├── core/            # Core filtering engine
├── parser/          # Output parsing (NDJSON, TAP, JUnit, etc.)
├── analytics/       # Token tracking + savings calculations
└── discover/        # "Missed savings" detection

The Hook System

The key innovation is the PreToolUse hook for Claude Code. When you run rtk init -g, it adds a hook to ~/.claude/settings.json that intercepts every Bash tool call:

  1. Claude decides to run git diff HEAD~3
  2. Before execution, the hook script (rtk-rewrite.sh) intercepts the command
  3. It rewrites it to rtk git diff HEAD~3
  4. The rewritten command executes, RTK filters the output
  5. Claude receives the compressed output — it never knows the rewrite happened

The hook also outputs permissionDecision: "allow", auto-approving the rewritten command. This is convenient but expands Claude Code's auto-approval surface (more on this in Security).

Filter System

RTK ships 60+ TOML-based filter definitions. Each filter specifies:

  • Match patterns: Which commands to intercept (e.g., cargo test, git status)
  • Output transformations: Regex-based rules to strip, group, or collapse output
  • Format-aware parsing: NDJSON, TAP, JUnit XML for test runners

Users can also define project-local filters in .rtk/filters.toml (requires SHA-256 trust verification) or global filters in ~/.config/rtk/filters.toml.

Tee: Failure Recovery

When a command fails, RTK saves the full unfiltered output to ~/.local/share/rtk/tee/. The compressed output includes a pointer to the full log:

FAILED: 2/15 tests
[full output: ~/.local/share/rtk/tee/1707753600_cargo_test.log]

This means the AI agent gets the compressed summary first, and can read the full output only if needed — a smart lazy-loading pattern.


Competitors & Alternatives

ToolApproachScopeToken Savings
RTKCLI proxy, output filteringShell commands only60-90%
HeadroomContext optimization layerEverything (JSON, code, logs, RAG, conversation)Varies
Morph CompactConversation compression SDKAgent conversation history50-70%
Claude Prompt CachingBuilt-in to ClaudeRepeated content (system prompts, files)Up to 90%
Model RoutingRoute by task difficultyAll LLM callsCost reduction via cheaper models
Subagent DelegationBuilt-in to Claude CodeVerbose tasksContext isolation

Headroom — The Closest Competitor

Headroom is a "context optimization layer" that compresses everything before it reaches the LLM: JSON arrays, code (AST-aware for Python/JS/Go/Rust), logs, RAG results, and conversation history. It uses ModernBERT-based compression.

RTK vs Headroom: They're actually complementary. RTK handles CLI command output specifically with hand-crafted filters. Headroom handles everything else with ML-based compression. You could run both.

Morph Compact

A hosted/SDK service that compresses agent conversation context 50-70% via verbatim deletion (no summarization). Focused on conversation-history bloat, not CLI output. Different problem space.

Built-in Alternatives (Free)

Before reaching for RTK, consider what's already available:

  • Prompt Caching: Claude automatically caches repeated content. Zero config, up to 90% cost reduction on cache hits.
  • Auto-compaction: Claude Code automatically compresses conversation history when approaching context limits.
  • Subagents: Delegate verbose tasks to subagents — only the summary returns to the main context.
  • .claudeignore: Exclude files and directories from being indexed.

Security Analysis

A community-initiated security audit (issue #640) found several concerns. Here's the honest breakdown:

Critical: Shell Injection

rtk err, rtk test, and rtk summary pass user-provided command strings directly to sh -c without sanitization. Since RTK's hook auto-rewrites commands from LLM output, this creates an LLM prompt injection -> shell injection chain.

// The problematic pattern:
Command::new("sh")
    .args(["-c", command])   // command is user-controlled, unescaped

Severity is elevated because the hook auto-approves commands, bypassing Claude Code's permission prompt.

High: Telemetry Concerns

  • Default telemetry (in Homebrew builds) sends a persistent device hash (SHA-256 of hostname + username) — linkable across sessions
  • top_commands reveals workflow patterns
  • No install-time consent dialog (opt-out requires editing config)

Mitigation:

# Disable telemetry
rtk telemetry disable

# Or via environment variable
export RTK_TELEMETRY_DISABLED=1

High: Token/Secret Leakage in Tracking DB

RTK stores full command strings (including arguments) in a SQLite database for 90 days. Commands like curl -H "Authorization: Bearer tok_abc" persist verbatim. The rtk gain --history command can surface these in LLM context.

Medium: Global Filters Not Integrity-Checked

Project-local .rtk/filters.toml requires SHA-256 verification, but ~/.config/rtk/filters.toml is trusted unconditionally. If malware modifies the global filter, it could suppress security scanner output or rewrite arbitrary command output — particularly dangerous since RTK sits in the LLM's output pipeline.


Trade-off Analysis

Pros

AspectDetail
Token savings60-90% reduction is real and significant for heavy users
Zero frictionInstall once, forget about it. Transparent hook system.
Performance<10ms overhead per command. Written in Rust. Single binary, zero dependencies.
Breadth100+ supported commands, 12+ AI tools, 60+ filter rules
Tee recoveryFull output saved on failure — no information loss when it matters
Open sourceMIT license, active community, 27k+ stars

Cons

AspectDetail
Security surfaceShell injection risk via sh -c, auto-approval widens attack surface
Information lossAggressive filtering can hide relevant context. If RTK strips something the LLM needed, you get worse results silently.
Hook bypassBuilt-in Claude Code tools (Read, Grep, Glob) bypass RTK entirely — inconsistent coverage
Telemetry defaultsOpt-out rather than opt-in for Homebrew builds
Secret retentionFull command strings stored for 90 days in SQLite
Debugging difficultyWhen something goes wrong, RTK adds another layer to debug through
Filter maintenanceFilters are hand-crafted per command — new tools need new filters

Who Should NOT Use RTK

  • Security-sensitive environments: The shell injection chain (LLM -> hook -> sh -c) is a real concern if you're running Claude Code on sensitive infrastructure.
  • Debugging-heavy workflows: If you frequently need full command output to debug issues, RTK's filtering adds friction.
  • Light AI coding usage: If you only use Claude Code occasionally, the token savings aren't worth the added complexity.

The Core Trade-off

RTK trades completeness for efficiency. You're betting that 80% of shell output is noise the LLM doesn't need. Most of the time, that's correct — the LLM doesn't need 15 lines of git push progress bars. But occasionally, the filtered-out information was exactly what the LLM needed to diagnose an issue. The tee system mitigates this for failures, but for successful commands that produce "too much" output, the information is simply gone.

The security trade-off is more nuanced: RTK's hook auto-approves rewritten commands, which is convenient but means you're trusting RTK's rewrite logic to never produce something dangerous. The sh -c shell injection finding shows this trust isn't fully warranted yet.


My Recommendation

For most AI coding power users: worth installing, with these precautions:

# Install
brew install rtk
rtk init -g

# Immediately disable telemetry
rtk telemetry disable

# Or set in your shell profile
echo 'export RTK_TELEMETRY_DISABLED=1' >> ~/.zshrc

Monitor your savings with rtk gain periodically. If you notice Claude struggling with filtered output, you can always exclude specific commands:

# ~/.config/rtk/config.toml
[hooks]
exclude_commands = ["curl", "playwright", "your-custom-tool"]

Links: