rtk Documentation

CLI proxy that reduces LLM token consumption by 60-90% on common dev commands. Single Rust binary, zero dependencies

RAW Doc

Technical Documentation: rtk-ai/rtk

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)


rtk filters and compresses command outputs before they reach your LLM context. Single Rust binary, 100+ supported commands, <10ms overhead.

What RTK Does

RTK intercepts shell commands and compresses their output before your agent reads it.

Operation What RTK does to the output
ls / tree Tree format with file counts instead of one line per entry
cat / read Smart file reading: signatures and structure over full bodies
grep / rg Truncates long lines, groups matches by file
ast-grep Groups structural matches by file, caps overflow
git status Compact stat format, grouped by state
git diff Reduced context, headers stripped
git log Hash, author and subject only
git add/commit/push Confirmation line instead of full progress output
cargo test / npm test Failures only, passing tests collapsed to a count
ruff check Grouped by rule and file
sqlfluff lint Grouped by rule and file
pytest Failures only, traceback trimmed
go test NDJSON parsed, failures only
docker ps Essential fields only

How Savings Work

RTK reduces the bash output your agent reads. That is what RTK measures, and it is not the same as cutting your bill by the same amount.

Bash output is one contributor to input tokens, alongside your prompt, the system prompt and conversation history. Input tokens are in turn only part of the bill, which also counts output tokens. The reduction dilutes at every step.

The token counts RTK reports are estimated as bytes / 4 — RTK ships no tokenizer, so the percentages are reliable but the absolute token numbers are approximate.

Full explanation: How RTK Savings Work

Installation

bash
brew install rtk                                              # macOS / Linux
winget install rtk-ai.rtk                                     # Windows
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh

Then check you have the right package — another project named "rtk" exists on crates.io:

bash
rtk --version   # any current version
rtk gain        # must show the savings dashboard, not "command not found"

Cargo, pre-built binaries, native Windows, WSL, and uninstall: Installation guide

What You Install

RTK ships as three downloads. The two rtk editions install a binary with the same name — hooks, scripts and agent config work unchanged across both — so they cannot be installed together. Uninstall one before installing the other.

mermaid
flowchart TD
    agent["Your coding agent"]

    subgraph editions["Pick one · both install a binary named rtk"]
        direction LR
        full["<b>rtk</b><br/>Apache-2.0 + BUSL-1.1<br/>hooks · filters · engine"]
        oss["<b>rtk-oss</b><br/>Apache-2.0 only<br/>hooks · filters"]
    end

    proxy["<b>rtk-proxy</b><br/>BUSL-1.1<br/>compresses API traffic"]

    agent -->|"command output"| full
    agent -->|"command output"| oss
    agent -.->|"API traffic"| proxy

    classDef apache stroke:#2da44e,stroke-width:2px,fill:none
    classDef busl stroke:#0969da,stroke-width:2px,fill:none
    classDef mixed stroke:#8250df,stroke-width:2px,fill:none
    classDef plain stroke:#8c959f,stroke-width:1px,fill:none
    class full mixed
    class oss apache
    class proxy busl
    class agent plain
    style editions fill:none,stroke:#d0d7de,stroke-dasharray:4 4
Download Licence Contains
rtk-<target> Apache-2.0 + BUSL-1.1 Hook core, every command filter, and the compression engine
rtk-oss-<target> Apache-2.0 only Hook core and every command filter. No engine.
rtk-proxy-<target> BUSL-1.1 The RTK Proxy daemon. Never bundled with either rtk.

Production use is free, engine and daemon included. What the BUSL grant does not cover is offering RTK or a derivative to third parties as a commercial product or service — for that, contact [email protected].

bash
curl -fsSL .../install.sh | RTK_EDITION=oss sh    # Apache-2.0 edition
brew install rtk-ai/tap/rtk-oss

What the Apache-2.0 edition omits and where the licence boundary falls: Editions and Licensing

Quick Start

bash
# 1. Install for your AI tool
rtk init -g                     # Claude Code / Copilot (default)
rtk init -g --gemini            # Gemini CLI
rtk init -g --codex             # Codex (OpenAI)
rtk init -g --agent cursor      # Cursor
rtk init -g --agent windsurf    # Windsurf
rtk init --agent cline          # Cline / Roo Code
rtk init --agent kilocode       # Kilo Code
rtk init --agent antigravity    # Google Antigravity
rtk init --agent kimi           # Kimi AI
rtk init -g --agent pi          # Pi
rtk init --agent omp            # Oh My Pi (OMP)
rtk init --agent hermes         # Hermes
rtk init -g --agent droid       # Factory Droid
rtk init --agent trae           # Trae (project: .trae/hooks.json)
rtk init -g --agent trae        # Trae global: ~/.trae and existing ~/.trae-cn

# 2. Restart your AI tool, then test
git status  # Automatically rewritten to rtk git status

The agent receives compact output without ever calling rtk explicitly.

The hook only fires on Bash tool calls. Built-in tools like Read, Grep and Glob do not pass through it. Use shell commands (cat, rg, find) or call rtk read, rtk grep, rtk find directly.

Per-agent setup, --dry-run, and uninstall: Quick Start · Supported Agents

How It Works

text
Without rtk:                                    With rtk:

  Claude  --git status-->  shell  -->  git         Claude  --git status-->  RTK  -->  git
    ^                                   |            ^                      |          |
    |         full raw output           |            |  compact output      | filter   |
    +-----------------------------------+            +------- (filtered) ---+----------+

Four strategies applied per command type:

  1. Smart Filtering - Removes noise (comments, whitespace, boilerplate)
  2. Grouping - Aggregates similar items (files by directory, errors by type)
  3. Truncation - Keeps relevant context, cuts redundancy
  4. Deduplication - Collapses repeated log lines with counts

Does RTK break Claude's prompt cache? No. RTK filters output once per command. The result is stored in history and cached normally on subsequent API calls, so the cache keeps working as expected. Smaller outputs also mean cheaper cache writes and reads. See Troubleshooting for details.

Supported AI Tools

RTK supports 18 AI coding tools. Each integration rewrites shell commands to rtk equivalents, reducing the bash output the agent reads where the agent supports command interception.

Tool Install Method
Claude Code rtk init -g PreToolUse hook (native binary)
GitHub Copilot (VS Code) rtk init -g --copilot PreToolUse hook — transparent rewrite
GitHub Copilot CLI rtk init -g --copilot PreToolUse deny-with-suggestion (CLI limitation)
Cursor rtk init -g --agent cursor preToolUse hook (hooks.json)
Gemini CLI rtk init -g --gemini BeforeTool hook
Codex rtk init -g --codex PreToolUse hook (updatedInput) + AGENTS.md
Windsurf rtk init -g --agent windsurf .windsurfrules (project-scoped)
Cline / Roo Code rtk init --agent cline .clinerules (project-scoped)
OpenCode rtk init -g --opencode Plugin TS (execute.before / tool.execute.before), needs 1.18.29 or newer
OpenClaw openclaw plugins install ./openclaw Plugin TS (before_tool_call)
Pi rtk init -g --agent pi (global) TypeScript extension (tool_call)
Oh My Pi (OMP) rtk init -g --agent omp (global) / rtk init --agent omp (project) TypeScript extension (tool_call, shared with Pi)
Hermes rtk init --agent hermes Python plugin adapter (terminal command mutation via rtk rewrite)
Mistral Vibe rtk init -g --agent vibe pre_tool hook (hooks.toml)
Kilo Code rtk init --agent kilocode .kilocode/rules/rtk-rules.md (project-scoped)
Google Antigravity rtk init --agent antigravity / rtk init -g --agent antigravity .agents/plugins/rtk/ (project) or ~/.gemini/config/plugins/rtk/ (global): plugin.json, hooks.json, rules/AGENTS.md
Kimi AI rtk init --agent kimi AGENTS.md (project-scoped)
Factory Droid rtk init -g --agent droid (or per-project) PreToolUse hook in ~/.factory/hooks.json (matcher Execute)
Trae rtk init --agent trae Native PreToolUse hook in .trae/hooks.json (RunCommand)
Trae (global) rtk init -g --agent trae ~/.trae/hooks.json, plus ~/.trae-cn/hooks.json when that directory exists

For per-agent setup details, override controls, and graceful degradation, see the Supported Agents guide. The Hermes plugin source and tests live in hooks/hermes/; installed Hermes runtime files still live under ~/.hermes/plugins/rtk-rewrite/.

What RTK Covers

100+ commands across Git, GitHub CLI, Cargo/Rust, JavaScript (npm, pnpm, vitest, jest, tsc, Next.js, Playwright, Prisma), Python (ruff, pytest, mypy, pip), Go (go test, golangci-lint), Ruby, .NET, JVM (Gradle, Maven), PHP, Docker, Kubernetes, AWS, Terraform, and the file/search builtins.

bash
-v, --verbose          # increase verbosity (-v, -vv, -vvv)
rtk bypass <cmd>       # run one command unfiltered, still tracked

Every command and its filter: What RTK Optimizes

Measure It

bash
rtk gain                        # Summary stats
rtk gain --graph                # ASCII graph (last 30 days)
rtk gain --history              # Recent command history
rtk gain --daily                # Day-by-day breakdown
rtk gain --all --format json    # JSON export for dashboards

rtk discover                    # Find missed savings opportunities
rtk discover --all --since 7    # All projects, last 7 days

rtk session                     # Show RTK adoption across recent sessions

Attribution, the live dashboard and export formats: Token Savings Analytics

RTK Proxy (optional)

rtk optimizes command output before your agent reads it. The rtk-proxy daemon extends the same engine to your agent's provider-API traffic — a local background process that sits between your agent and the provider and runs the pipeline on the traffic itself: it compresses payloads, strips bytes no model ever needed before forwarding, and carries the calls your pipeline routes to cheaper models, so they borrow the credential your agent already sends and show up in rtk gain. It can also align prompt-cache breakpoints where the agent leaves them open (opt-in).

bash
rtk claude              # start the daemon if needed, then launch Claude through it
rtk llm-proxy status    # is it up
rtk gain                # savings, command-filtering and proxy side by side

Two independent ways in, because not every harness has a session hook:

What it does
rtk init -g Offers to set it up once: writes the base URL and the rtk:proxy pipeline, then asks how the daemon should come back after a reboot — SessionStart hook, background service, or neither.
rtk <harness> No configuration at all. Starts the daemon, points the harness at it, then execs it. Needs no hook system, which is why it covers harnesses that have none.

rtk <harness> resolving is itself the signal that traffic really goes through the daemon — the subcommand exists only where redirection is verified. Today that is rtk claude. A harness without one is never silently unproxied: it falls through to plain passthrough, and rtk llm-proxy setup lists what can be launched.

It is a separate binary under a separate licence (BUSL-1.1), shipped as its own rtk-proxy-<target> archive — rtk never links it.

Setup and subcommands: RTK Proxy · what it compresses: What the Proxy Compresses · settings and endpoints: Proxy Configuration

The Engine (`pipeline.toml`)

Everything RTK does — which commands get rewritten, what happens to a tool's output, what the proxy compresses — is expressed in one composable file. The engine (engine/, crate rtk-engine, BUSL-1.1) reads it, so the levers are configurable instead of hard-coded.

Upgrading changes nothing. With no pipeline file you are on rtk:default: the same PreToolUse rewrite, the same awareness file, your filters.toml, your config.toml. Everything beyond that is opt-in, one line at a time:

toml
# ~/.config/rtk/pipeline.toml
[pipeline]
extends = ["rtk:posthook", "rtk:proxy"]

The tour: Pipelines · the reference: Writing a Pipeline, Conditions and Judges, Methods, Tasks and Upstreams · the commands: Pipeline Commands · write your own method: Plugins

Documentation

rtk-ai.app/guide is the full user guide.

Installation every install method, Windows, WSL, uninstall
Quick Start per-agent init, --dry-run, first savings
Supported Agents setup detail, override controls, degradation
Agent Coverage what each agent's hook API lets RTK do
What RTK Optimizes every command and its filter
Configuration config.toml, env vars, recall recovery
Token Savings Analytics rtk gain in full
Pipelines the engine: pipeline.toml, rules, judges, tasks, sharing
RTK Proxy the daemon: setup, what it compresses, settings
Editions and Licensing what each download contains
Telemetry & Privacy what is collected, how to opt in, your GDPR rights
Troubleshooting common issues, prompt-cache questions

Repository references: engine/README.md · proxy/README.md · ARCHITECTURE.md · CONTRIBUTING.md · SECURITY.md

RTK collects no command content and no file contents. Telemetry is anonymous and off by default — it sends nothing until you opt in with explicit consent (GDPR Art. 6 & 7) via rtk telemetry enable, and documented in Telemetry & Privacy.

Star History

StarMapper

Core team

Contributing

Contributions welcome! Please open an issue or PR on GitHub.

Join the community on Discord.

License

The rtk CLI is Apache-2.0 — see LICENSE.

It links one component under a different licence. rtk-engine (engine/) is
BUSL-1.1, source-available: see engine/LICENSE. Its
Additional Use Grant permits production use as part of the rtk binary, so
running rtk is covered; using the engine on its own, or inside another
product, is not until the Change Date.

The rtk-proxy daemon (proxy/) is BUSL-1.1 on the same terms and ships as
its own archive with its licence alongside — rtk never links it.

Disclaimer

See DISCLAIMER.md.


Overview (`engine/README.md`)

rtk-engine

The RTK pipeline engine: it parses, resolves and validates pipeline.toml, evaluates rules, and runs the methods and chains those rules name. The rtk CLI links it for the hooks; the rtk-proxy daemon links it for proxy.request rules.

User documentation

The pipeline reference lives in the user guide, docs/guide/pipelines/, published at rtk-ai.app/guide:

Page Covers
Pipelines what the engine does, the files you might write, upgrading
Writing a Pipeline file locations and load order, the rule tree, actions, routing, overrides
Conditions and Judges every when key, the call context, classifiers
Methods built-in methods and chains, your own chains and filters
Directives text RTK puts in front of the agent
Tasks and Upstreams delegation to a cheaper model
Reference Pipelines the six built-ins as literal files
Pipeline Commands show, validate, explain, shadow, share, adopt
Errors and Warnings every diagnostic code, limits
Plugins new methods and task kinds in Rust

Worked pipelines: examples/pipelines/.

Contributing

See CONTRIBUTING.md. rtk methods list prints every registered method with its contract.

License

Business Source License 1.1 — source-available, not OSI open source. Production use is free; offering the engine, or a derivative, to third parties as a paid or hosted product needs a licence from [email protected]. On 2030-09-09 this version converts to Apache-2.0. The rtk CLI's own source is Apache-2.0, and its Apache-2.0 edition is built without this crate — see Editions and Licensing.


Overview (`hooks/README.md`)

LLM Agent Hooks

Scope

Deployed hook artifacts — the files and agent-specific configuration installed on user machines by rtk init. External scripts and plugins are thin delegates to rtk rewrite; native processors such as Codex call the same Rust rewrite registry directly. Zero filtering logic is duplicated in this directory.

Contains per-agent hook scripts and configuration files for 13 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi, Oh My Pi, Mistral Vibe, Trae, Google Antigravity). rtk init installs them.

Does not own: hook installation/uninstallation (that's src/hooks/init/), the rewrite pattern registry (that's discover/registry), or integrity verification (that's src/hooks/integrity.rs).

Awareness files

rtk-awareness.md, rtk-awareness-high.md, rtk-awareness-full.md are the agent-neutral
instruction files embedded by rtk init (RTK.md, GEMINI.md, rules files, AGENTS.md blocks).
awareness.level in config.toml picks one for hook-based agents; agents without a hook always get
rtk-awareness-full.md, the only one that tells the agent to prefix commands with rtk.

Purpose

LLM agent integrations that intercept CLI commands and route them through RTK for token optimization. Each hook transparently rewrites raw commands (e.g., git status) to their RTK equivalents (e.g., rtk git status), cutting the bash output that reaches the LLM context without requiring the agent or user to change their workflow.

How It Works

text
Agent runs command (e.g., "cargo test --nocapture")
  -> Hook intercepts (PreToolUse / plugin event)
  -> Reads JSON input, extracts command string
  -> Uses the shared RTK rewrite registry
  -> Registry matches pattern, returns "rtk cargo test --nocapture"
  -> Hook sends response in agent-specific JSON format
  -> Agent executes "rtk cargo test --nocapture" instead
  -> Filtered output reaches LLM

All rewrite logic lives in the rtk binary. External hook scripts call rtk rewrite; native Rust hook processors call the registry directly. This keeps one source of truth for all 70+ rewrite patterns.

Directory Structure

Each agent subdirectory has its own README with hook-specific details:

  • claude/ — Shell hook, PreToolUse JSON format, settings.json patching, test script
  • copilot/ — Rust binary hook, dual format (VS Code Chat vs Copilot CLI), deny-with-suggestion fallback
  • cursor/ — Shell hook, Cursor JSON format, empty {} response requirement
  • cline/ — Rules file (prompt-level), .clinerules project-local installation
  • windsurf/ — Rules file (prompt-level), .windsurfrules workspace-scoped
  • codex/ — Rust binary hook (rtk hook codex), PreToolUse.updatedInput, .codex/hooks.json / $CODEX_HOME/hooks.json, plus AGENTS.md awareness
  • opencode/ — TypeScript plugin, OpenCode 2.0 and 1.x >= 1.18.29, in-place mutation
  • pi/ — TypeScript extension, tool_call event, local isBashToolCallEvent guard, in-place mutation, ~/.pi/agent/extensions/; shared with Oh My Pi (OMP) — OMP installs the same file at .omp/extensions/ via its legacy-pi-compat layer
  • hermes/ — Python plugin, pre_tool_call hook, in-place terminal command mutation
  • vibe/ — Rust binary hook (rtk hook vibe), pre_tool entry in ~/.vibe/hooks.toml, hook_specific_output.tool_input rewrite plus system_message for UI visibility
  • antigravity/ — Rust binary hook (rtk hook antigravity), PreToolUse plugin entry in .agents/plugins/rtk/hooks.json or ~/.gemini/config/plugins/rtk/hooks.json, transparent rewrite via overwrite.CommandLine

Supported Agents

Agent Mechanism Hook Type Can Modify Command?
Claude Code Shell hook (PreToolUse) Transparent rewrite Yes (updatedInput)
VS Code Copilot Chat Rust binary (rtk hook copilot) Transparent rewrite Yes (updatedInput)
GitHub Copilot CLI Rust binary (rtk hook copilot) Deny-with-suggestion No (agent retries)
Cursor Rust binary Transparent rewrite Yes (updated_input)
Trae Rust binary (rtk hook trae) Transparent rewrite Yes (updatedInput)
Google Antigravity Rust binary (rtk hook antigravity) Transparent rewrite Yes (overwrite.CommandLine)
Gemini CLI Rust binary (rtk hook gemini) Transparent rewrite Yes (hookSpecificOutput)
Cline / Roo Code Custom instructions (rules file) Prompt-level guidance N/A
Windsurf Custom instructions (rules file) Prompt-level guidance N/A
Codex CLI Rust binary (rtk hook codex) Transparent rewrite Yes (updatedInput)
OpenCode TypeScript plugin (execute.before in 2.0, tool.execute.before in 1.x >= 1.18.29) In-place mutation Yes
Pi TypeScript extension (tool_call event) In-place mutation Yes
Oh My Pi (OMP) TypeScript extension (tool_call event, shared with Pi) In-place mutation Yes
Hermes Python plugin (pre_tool_call) In-place mutation Yes
Mistral Vibe Rust binary (rtk hook vibe) Transparent rewrite Yes (hook_specific_output.tool_input)

JSON Formats by Agent

Claude Code (Shell Hook)

Input (stdin):

json
{
  "tool_name": "Bash",
  "tool_input": { "command": "git status" }
}

Output (stdout, when rewritten):

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "RTK auto-rewrite",
    "updatedInput": { "command": "rtk git status" }
  }
}

Cursor (Shell Hook)

Input: Same as Claude Code.

Output (stdout, when rewritten):

json
{
  "permission": "allow",
  "updated_input": { "command": "rtk git status" }
}

Returns {} when no rewrite (Cursor requires JSON for all paths).

Trae (Rust Binary)

Install with rtk init --agent trae for .trae/hooks.json, or rtk init -g --agent trae for ~/.trae/hooks.json; global installation also updates ~/.trae-cn/hooks.json when that directory already exists.

Input: Trae PreToolUse payloads for RunCommand, such as:

json
{
  "tool_name": "RunCommand",
  "tool_input": { "command": "git status", "description": "Check status" }
}

Output (only when rewritten):

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "updatedInput": { "command": "rtk git status", "description": "Check status" }
  }
}

RTK deliberately does not set permissionDecision; Trae retains its own command-approval policy.

Codex CLI (Rust Binary)

Input (stdin):

json
{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "permission_mode": "default",
  "tool_input": { "command": "git status" }
}

Output (stdout, when rewritten):

json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "RTK auto-rewrite",
    "updatedInput": { "command": "rtk git status" }
  }
}

The allow value is required by Codex to accept updatedInput; Codex still runs its native approval and sandbox checks after applying the replacement. Those checks classify the rewritten command, and Codex does not currently unwrap the rtk binary for its safe/dangerous-command heuristics. Missing or unknown permission modes and other no-rewrite cases produce no stdout.

Copilot CLI (Rust Binary)

Input (stdin, camelCase, toolArgs is JSON-stringified):

json
{
  "toolName": "bash",
  "toolArgs": "{\"command\": \"git status\"}"
}

Output (no updatedInput support -- uses deny-with-suggestion):

json
{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Token savings: use `rtk git status` instead"
}

VS Code Copilot Chat (Rust Binary)

Input (stdin, snake_case):

json
{
  "tool_name": "Bash",
  "tool_input": { "command": "git status" }
}

Output: Same as Claude Code format (with updatedInput).

Gemini CLI (Rust Binary)

Input (stdin):

json
{
  "tool_name": "run_shell_command",
  "tool_input": { "command": "git status" }
}

Output (when rewritten):

json
{
  "decision": "allow",
  "hookSpecificOutput": {
    "tool_input": { "command": "rtk git status" }
  }
}

No rewrite: {"decision": "allow"}

Mistral Vibe (Rust Binary)

Input (stdin):

json
{
  "tool_name": "bash",
  "tool_input": { "command": "git status" },
  "hook_event_name": "pre_tool",
  "session_id": "..."
}

Output (when rewritten):

json
{
  "hook_specific_output": {
    "tool_input": { "command": "rtk git status" }
  },
  "system_message": "rtk: rewrote to `rtk git status`"
}

No rewrite: exit 0 with empty stdout (Vibe's contract for "no opinion" from a pre_tool hook).

Google Antigravity (Rust Binary)

Input (stdin):

json
{
  "toolCall": {
    "name": "run_command",
    "args": { "CommandLine": "git status" }
  }
}

Output (when rewritten):

json
{
  "decision": "allow",
  "reason": "Rewritten by rtk for token optimization.",
  "overwrite": {
    "CommandLine": "rtk git status"
  }
}

No rewrite: {"decision": "allow"}

OpenCode (TypeScript Plugin)

Delegates to rtk hook opencode via node:child_process.execFile (supports OpenCode 2.0 and 1.x >= 1.18.29). The Rust side owns the rewrite rules and the permission verdict; the plugin only carries it. The hook must be awaited — args.command = … after the call returns has nothing left to rewrite:

typescript
const tool = String(input?.tool ?? "").toLowerCase()
if (tool === "bash" || tool === "shell") {
  // 2.x also has `event.agent`, which rtk needs so an agent-scoped ask or deny
  // is judged against the agent that runs the command. 1.x sends no agent.
  const argv = ["hook", "opencode"]
  if (agent) argv.push("--agent", agent)
  argv.push(command)
  const rewritten = await new Promise((resolve) => {
    execFile(rtkBin, argv, { encoding: "utf8", timeout: 3000, windowsHide: true }, (err, stdout) => {
      if (err) return resolve(null) // pass through unchanged
      try {
        resolve(JSON.parse(String(stdout ?? "").trim() || "{}").command ?? null)
      } catch {
        resolve(null)
      }
    })
  })
  // {} means the rewrite would change OpenCode's own verdict — run as typed.
  if (rewritten && rewritten !== command) {
    args.command = rewritten
  }
}

Hermes (Python Plugin)

Mutates args["command"] in-place via the pre_tool_call hook:

python
result = subprocess.run(["rtk", "rewrite", command], capture_output=True, text=True, timeout=2)
rewritten = result.stdout.strip()
if result.returncode in {0, 3} and rewritten and rewritten != command:
    args["command"] = rewritten

Command Rewrite Registry

The registry handles command patterns across these categories:

Category Examples
Test Runners vitest, pytest, cargo test, go test, playwright
Build Tools cargo build, npm, pnpm, dotnet, make
VCS git status/log/diff/show
Language Servers tsc, mypy
Linters eslint, ruff, golangci-lint, biome
Package Managers pip, cargo install, pnpm list
File Operations ls, find, grep, cat, head, tail
Infrastructure docker, kubectl, aws, terraform

Compound Command Handling

The registry handles &&, ||, ;, |, |&, and & operators:

  • Pipe (|): Producers and intermediate stages stay raw; only a pipeline-safe final stage is rewritten
  • Stderr pipe (|&): The complete pipeline stays raw
  • And/Or/Semicolon (&&, ||, ;): Both sides rewritten independently
  • Pipeline-safe rules: Initially limited to argument-safe grep, rg, and wc invocations; search pattern-file forms defer

Example: cargo fmt --all && cargo test becomes rtk cargo fmt --all && rtk cargo test

Override Controls

  • RTK_DISABLED=1: Per-command override (RTK_DISABLED=1 git status runs raw)
  • exclude_commands: In ~/.config/rtk/config.toml, list commands to never rewrite. Matches against the full command after stripping env prefixes. Subcommand patterns work ("git push" excludes git push origin main). Patterns starting with ^ are treated as regex.
  • Already-RTK: rtk git status passes through unchanged (no rtk rtk git)

Exit Code Contract

Hooks must never block command execution. All error paths (missing binary, bad JSON, rewrite failure) must exit 0 so the agent's command runs unmodified. A hook that exits non-zero prevents the user's command from executing.

When there is no rewrite to apply, the hook must produce no output (or {} for Cursor, which requires JSON on all paths).

Graceful Degradation

Hooks are non-blocking -- they never prevent a command from executing:

  • jq not installed: warning to stderr, exit 0 (command runs raw)
  • rtk binary not found: warning to stderr, exit 0
  • rtk version too old (< 0.23.0): warning to stderr, exit 0
  • Invalid JSON input: pass through unchanged
  • rtk rewrite crashes: hook exits 0 (subprocess error ignored)
  • Filter logic error: fallback to raw command output

Adding a New Agent Integration

New integrations must follow the Exit Code Contract and Graceful Degradation above, as well as the project's Design Philosophy.

Integration Tiers

Tier Mechanism Maintenance Examples
Full hook Shell script or Rust binary, intercepts commands via agent's hook API High — must track agent API changes Claude Code, Cursor, Copilot, Gemini, Codex, Antigravity
Plugin TypeScript/JS/Python plugin in agent's plugin system Medium — agent manages loading OpenCode, Hermes, Pi, Oh My Pi
Rules file Prompt-level instructions the agent reads Low — no code to break Cline, Windsurf

Eligibility

RTK supports AI coding assistants that developers actually use day-to-day. To add a new agent:

  • Agent has a documented, stable hook/plugin API (not experimental/alpha)
  • Agent is actively maintained (commit activity in last 3 months)
  • Integration follows the exit code contract (exit 0 on all error paths)
  • Hook output matches the agent's expected JSON format exactly

Maintenance

If an agent's API changes and the hook breaks, the integration should be updated promptly. If the agent becomes unmaintained or the hook can't be fixed, the integration may be deprecated with a release note.


Overview (`openclaw/README.md`)

RTK Plugin for OpenClaw

Transparently rewrites shell commands executed via OpenClaw's exec tool to their RTK equivalents, so filtered output reaches the LLM context.

How it works

The plugin registers a before_tool_call hook that intercepts exec tool calls. When the agent runs a command like git status, the plugin delegates to rtk rewrite which returns the optimized command (e.g. rtk git status). The compressed output enters the agent's context window, saving tokens.

All rewrite logic lives in RTK itself (rtk rewrite). This plugin is a thin delegate -- when new filters are added to RTK, the plugin picks them up automatically with zero changes.

Installation

Prerequisites

RTK must be installed and available in $PATH:

bash
brew install rtk
# or
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh

Install the plugin

bash
# Copy the plugin to OpenClaw's extensions directory
mkdir -p ~/.openclaw/extensions/rtk-rewrite
cp openclaw/index.ts openclaw/openclaw.plugin.json ~/.openclaw/extensions/rtk-rewrite/

# Restart the gateway
openclaw gateway restart

Or install via OpenClaw CLI

bash
openclaw plugins install ./openclaw

Configuration

In openclaw.json:

json5
{
  plugins: {
    entries: {
      "rtk-rewrite": {
        enabled: true,
        config: {
          enabled: true,    // Toggle rewriting on/off
          verbose: false     // Log rewrites to console
        }
      }
    }
  }
}

Permissions

RTK keeps the deny gate. OpenClaw owns approval.

The plugin runs rtk rewrite with RTK_REWRITE_HOST=openclaw. That tells RTK this host applies its own exec policy -- tools.exec.mode, security, ask -- to whatever the before_tool_call hook returns, so RTK does not prompt for a command that matched no rule.

Without it, RTK evaluates every command against Claude Code's four settings files (.claude/settings.json, .claude/settings.local.json, and the two under ~/.claude/) and returns "ask" for anything they do not explicitly allow. The plugin turned that into a blocking approval that denied on timeout, so a host running tools.exec.mode=full still stopped on every rewritable command, waiting on a decision derived from another agent's config file. See #3908.

What does not change:

  • A command matching a permissions.deny rule in those Claude Code settings files is still refused, and the plugin blocks the tool call. As in Claude Code, a rule matches the command as written rather than every way of invoking the program — Bash(git push *) does not stop git -C . push — so a deny rule is not a security boundary.
  • A command matching a permissions.ask rule you wrote still prompts when RTK rewrites it: RTK returns exit 3 and the plugin raises its approval request. Only the default ask -- no rule matched -- is relaxed. A command RTK does not rewrite passes through to OpenClaw's own policy unchanged.
  • A command containing a command substitution (`, $(...)) or a redirect to a file is never rewritten, on any host.

What does change: the plugin no longer prompts for a command that matched no rule. Any approval prompt you still see comes from OpenClaw itself, or from an explicit ask rule.

Writing exec rules

The plugin replaces params.command in before_tool_call, and OpenClaw carries hook adjustments forward into the parameters it passes to the exec tool. The tool therefore receives rtk git push, not git push, and the checks the exec tool runs on its own parameters -- tools.exec.mode, tools.exec.security, tools.exec.ask, and the exec-approvals allowlist -- match against the rtk form. Write those rules against the rtk form. This was already true before the permission change.

One gate runs earlier and sees the original command: a trusted tool policy registered with api.registerTrustedToolPolicy(...). OpenClaw runs trusted policies before ordinary before_tool_call hooks, so such a policy is shown git push, not rtk git push. Only the exec tool's own checks see the rewritten string.

Upgrading from an earlier build

An rtk older than RTK_REWRITE_HOST cannot collapse the default ask, so it
prompts for more commands than a current one; the plugin keeps treating exit 3
as "rewrite it, require approval", and upgrading is what removes the redundant
prompt. Nothing else changes on upgrade.

rtk version

No minimum. RTK_REWRITE_HOST travels in the environment rather than in argv precisely so that an rtk which does not know it simply ignores it: you get the previous behaviour, a prompt on exit 3, rather than a gate that silently stops matching. The exit-code table in src/hooks/README.md lists what each exit means for a delegate.

What gets rewritten

Everything that rtk rewrite supports (30+ commands). See the full command list.

What's NOT rewritten

Handled by rtk rewrite guards:

  • Commands already using rtk
  • Piped commands (|, &&, ;)
  • Heredocs (<<)
  • Commands without an RTK filter

License

Apache 2.0 -- same as RTK.


Overview (`proxy/README.md`)

rtk-proxy


rtk optimizes command output before it reaches your assistant's context.
rtk-proxy brings the same engine to the API traffic itself: it sits
between your AI coding tool and the real provider endpoint, runs the pipeline
on the traffic — compression, and the relay of calls your pipeline routes to
cheaper models — then relays the response back. It is agent-agnostic by
design: it speaks the provider's dialect, not the agent's. Today Claude Code
is wired, through rtk claude; other agents follow as their redirection is
verified end to end.

It is a local daemon: one background process on your machine, driven by the
rtk CLI. It does four things: intercept, compress, forward, record.

text
Claude Code ──▶ rtk-proxy ──▶ api.anthropic.com
                     │
                     ├─ normalize: strip ANSI / redraws / dupes
                     ├─ your proxy.request.tool_result method rules
                     ├─ (opt-in) cache-align
                     └─ record usage ──▶ rtk gain

User documentation

Setup, what the daemon compresses, its settings and endpoints live in the user guide, docs/guide/proxy/, published at rtk-ai.app/guide:

Page Covers
RTK Proxy install, rtk claude, rtk init -g, --env, binding, state, savings
What the Proxy Compresses normalisation, cache alignment, your proxy.request rules
Proxy Configuration settings, provider dialects, HTTP endpoints, attribution headers
bash
rtk claude              # start the daemon if needed, then launch Claude through it
rtk llm-proxy status    # is it up
rtk gain                # savings, RTK and proxy side by side

Building

The crate lives in the rtk repository as its own workspace, and is not
linked into the rtk CLI — rtk locates and spawns this binary.

bash
cargo build --release --manifest-path proxy/Cargo.toml   # the daemon
cargo test --manifest-path proxy/Cargo.toml              # unit + functional + real-binary smoke

Contributing to the daemon: see docs/contributing/.

License

Business Source License 1.1 — source-available, not an
OSI-approved open-source license. Its Additional Use Grant permits free
production use
, including running the daemon. What it does not permit is
offering rtk-proxy, or a derivative, to third parties as a paid or hosted
product or service — for that, contact [email protected]. On
2030-09-09 this version converts automatically to Apache-2.0.

This is a different license from the rtk CLI in the parent directory, which
is Apache-2.0. The two are separate crates and separate binaries; rtk never
links this code.


Overview (`examples/pipelines/README.md`)

Pipeline examples

Example pipeline files. For how pipeline files work, see
Pipelines.
Copy one to ~/.config/rtk/pipeline.toml (or drop several into
~/.config/rtk/pipelines/) and run rtk pipeline validate.

Writing nothing is also a configuration: with no pipeline file you are on
rtk:default — RTK exactly as it works today.

File What it does
opt-in.toml turn on output compression (rtk:posthook) and the proxy (rtk:proxy)
tune-one-thing.toml output compression plus delegated reads, with the oversize-read threshold raised by one [override] line
delegate-reads-adoption.toml oversize reads route to a cheap model you bind
classify-routing.toml a Jev classifier labels shell output; failing test runs route to a digest task
laya-route.toml a local System-1 judge (Laya) gates routing of heavy shell output to Haiku/Sonnet digest workers — see its header for the prerequisite (local Laya, or hosted Jev + token)
judge-before-tool.toml a judge decides before a read, command or fetch runs whether a cheaper model handles it; fail-open
complex-session.toml a Jev classifier drives a custom plugin method plus built-in chains per output kind — needs a build with --features example-plugins

The six built-in reference pipelines are compiled in — print any of them
with rtk pipeline show rtk:default / rtk:posthook / rtk:proxy /
rtk:delegate-reads / rtk:verbatim-diffs / rtk:terse.


Overview (`hooks/antigravity/README.md`)

Google Antigravity Hooks

Part of hooks/

Specifics

  • Native programmatic PreToolUse lifecycle hook (rtk hook antigravity) providing transparent command rewriting via overwrite.CommandLine
  • Supported across all Antigravity surfaces: Antigravity CLI (agy), Antigravity IDE, and Antigravity 2.0
  • Installs the awareness file selected by awareness.level as rules/AGENTS.md inside the plugin (plain markdown, applied whenever the plugin is active)
  • Uses Antigravity's modular Plugin architecture:
    • Local workspace: .agents/plugins/rtk/
    • Global scope: ~/.gemini/config/plugins/rtk/
  • Full lifecycle support:
    • Setup: rtk init --agent antigravity (or rtk init -g --agent antigravity)
    • Preview: rtk init --agent antigravity --dry-run
    • Uninstallation: rtk init --agent antigravity --uninstall (or rtk init -g --agent antigravity --uninstall)

Permission Evaluation Note

Antigravity evaluates its tool permissions after lifecycle hooks rewrite commands. If you maintain strict command allowlists, ensure permitted commands account for rtk (e.g. command(rtk git status) or command(rtk *)).


Overview (`hooks/claude/README.md`)

Claude Code Hooks

Part of hooks/

Specifics

  • Shell-based PreToolUse hook -- requires jq for JSON parsing
  • Returns updatedInput JSON for transparent command rewrite (agent doesn't know RTK is involved)
  • Exits silently (exit 0) on any failure: jq missing, rtk missing, rtk too old (< 0.23.0), no match
  • Version guard checks rtk --version against minimum 0.23.0
  • ../rtk-awareness.md (or -high / -full, per awareness.level in config.toml) is the instructions file embedded into CLAUDE.md by rtk init

Overview (`hooks/cline/README.md`)

Cline / Roo Code Hooks

Part of hooks/

Specifics

  • Prompt-level guidance only (no programmatic hook) -- relies on Cline reading custom instructions
  • Installs ../rtk-awareness-full.md (shared, agent-neutral): the instruction to prefix every shell command with rtk, plus meta commands. No hook, so the full level is always used regardless of awareness.level
  • Installed to .clinerules (project-local) by rtk init

Overview (`hooks/codex/README.md`)

Codex CLI Hooks

Part of hooks/

Specifics

  • Native Rust PreToolUse processor: rtk hook codex
  • Transparently rewrites tool_input.command with Codex's updatedInput response
  • Registers a Bash matcher in .codex/hooks.json (project) or $CODEX_HOME/hooks.json (global)
  • Writes the shared awareness document selected by awareness.level to RTK.md, referenced from AGENTS.md
  • Installed by rtk init --codex (project) or rtk init -g --codex (global)
  • Uninstalled by adding --uninstall to the corresponding project or global command

Codex requires permissionDecision: "allow" in the hook response for updatedInput to take effect. Codex applies the replacement before its normal command approval and sandbox checks, so those native checks still run on the rewritten command. Codex's command safety classifier does not currently unwrap the rtk binary, so classification is based on the rewritten command. This can add prompts for known-safe commands or obscure signals for wrapped mutating commands such as git push.

RTK rewrites the documented Codex permission modes. Missing or unknown permission modes, no match, malformed JSON, unsupported commands, heredocs, substitutions, and file redirections fail open: the hook exits successfully without stdout and Codex executes the original command.

RTK does not parse Codex execution rules or reuse another agent's permission
files: Codex's native execution layer remains responsible for those rules.

History in workspace-write sandboxes

Command rewriting can work even when the sandbox prevents RTK from writing its
SQLite history database. If recording reports SQLITE_CANTOPEN, run
rtk init --codex outside the sandbox to see an optional Codex configuration
snippet using the actual database parent directory. The path follows
RTK_DB_PATH, then tracking.database_path, then the platform default.

To opt in, merge the printed sandbox_workspace_write.writable_roots entry into
your Codex config.toml, preserving existing entries. SQLite needs directory
access for journal/WAL files as well as the database itself. RTK only prints the
snippet; it does not change sandbox settings. Review custom paths before granting
access, and prefer a dedicated RTK data directory.

See the Codex configuration reference.


Overview (`hooks/copilot/README.md`)

GitHub Copilot Hooks

Part of hooks/

Specifics

  • Uses the rtk hook copilot Rust binary (not a shell script) -- no jq dependency
  • Auto-detects two input formats: VS Code Copilot Chat (snake_case tool_name/tool_input) and Copilot CLI (camelCase toolName/toolArgs with JSON-stringified args)
  • VS Code format: returns updatedInput for transparent rewrite
  • Copilot CLI format: returns permissionDecision: "deny" with suggestion (Copilot CLI API doesn't support updatedInput)

Overview (`hooks/cursor/README.md`)

Cursor IDE Hooks

Part of hooks/

Specifics

  • Same delegating pattern as Claude Code hook but outputs Cursor's JSON format (permission/updated_input instead of hookSpecificOutput/updatedInput)
  • Returns {} (empty JSON) when no rewrite applies -- Cursor requires JSON output for all code paths
  • Requires jq and rtk >= 0.23.0

Overview (`hooks/hermes/README.md`)

RTK Plugin for Hermes

Rewrites Hermes terminal tool commands to RTK equivalents before execution, so Hermes receives compact command output without changing your workflow.

Installation

bash
rtk init --agent hermes

The installer writes the plugin to ~/.hermes/plugins/rtk-rewrite/ and enables it through plugins.enabled in the Hermes config.

How it works

Hermes loads plugins from Python, so the plugin entrypoint is Python. The Python code is only a thin Hermes adapter. It reads the Hermes terminal tool payload, calls rtk rewrite for the actual command decision, then mutates the terminal tool command before Hermes executes it.

All rewrite rules stay in Rust inside rtk rewrite. When RTK adds or changes command rewrite behavior, the Hermes plugin picks up that behavior by delegating to the RTK binary.

Fail-open behavior

The plugin does not block command execution. If anything goes wrong, Hermes runs the original command unchanged.

If rtk is not available in PATH when Hermes loads the plugin, the plugin prints a warning and skips hook registration.

  • rtk is missing from PATH
  • rtk rewrite exits with an error
  • Hermes sends a non-terminal tool call
  • The tool payload has no string command
  • The plugin raises an unexpected exception

Limitations

  • Only Hermes terminal tool calls are rewritten.
  • Commands skipped by rtk rewrite stay unchanged, including commands already prefixed with rtk, compound shell commands, heredocs, and commands without an RTK filter.
  • Shell hooks are not used for Hermes command rewriting. The integration depends on Hermes loading Python plugins and passing a mutable terminal tool payload.

Overview (`hooks/kilocode/README.md`)

Kilo Code Hooks

Part of hooks/

Specifics

  • Prompt-level guidance only (no programmatic hook) -- relies on Kilo Code reading custom instructions
  • Installs ../rtk-awareness-full.md (shared, agent-neutral): the instruction to prefix every shell command with rtk, plus meta commands. No hook, so the full level is always used regardless of awareness.level
  • Installed to .kilocode/rules/rtk-rules.md (project-local) by rtk init --agent kilocode

Overview (`hooks/opencode/README.md`)

OpenCode Hooks

Part of hooks/

Specifics

  • TypeScript plugin (not a shell hook)
  • Supports OpenCode 2.0 (execute.before via { id, setup }) and OpenCode 1.x >= 1.18.29 (tool.execute.before), the dual-shape default export OpenCode's own V2 docs prescribe
  • Requires OpenCode 1.18.29 or newer, the floor OpenCode's V2 plugin docs give for this dual-shape export. With an older OpenCode, rtk init -g --opencode writes nothing and leaves an existing plugin alone, when it can read opencode --version
  • Thin delegating shim: calls rtk hook opencode as a subprocess, and the Rust side is the single source of truth for rewrite rules
  • The Rust side judges the command against OpenCode's own permission rules and answers {} whenever the rewrite would change the verdict those rules give — OpenCode evaluates the final command itself, so RTK never lifts a deny, silences an ask, or blocks an allow (#4195). Rules are read from root and project opencode.json/.jsonc in the order OpenCode resolves them: the legacy permission map first, then the 2.x permissions list, whatever order the file declares them. Every file's root rules load before any file's agent rules, so a project allow cannot outrank a global agent ask. 2.x list actions match as globs against shell, so sh* covers it; a list rule whose action only names bash does not count, because OpenCode ignores it there. A legacy map key the two generations read differently (shell and sh* apply only on 2.x, b* only on 1.x) makes rtk leave the command as typed, because it cannot tell which generation it is serving. Global rules come before project rules. An agent defined as a markdown file in agents/ or agent/, under ~/.config/opencode/ or any .opencode/ from the working directory up, whose frontmatter declares permission: or permissions:, is not parsed here, so no rewrite happens for that agent and OpenCode applies its own rules.
  • Uses standard node:child_process.execFile (compatible with OpenCode CLI, OpenCode Desktop/Electron, and Windows; zero Bun/zx dependencies)
  • Passes the command as a single argv element, never through a shell
  • Resolves rtk from the system PATH only (PATHEXT on Windows), and skips a PATH entry that is a directory. OpenCode's shell tool runs with OpenCode's own PATH and never sources .profile/.bashrc, so an rtk found outside PATH cannot be spawned as a bare rtk … — see #4462
  • Probes the resolved binary once per session with rtk hook opencode --help (cached): exit 0 means the subcommand is there, exit 2 means it predates it, and a broken or wrong-arch binary fails to spawn. Not a version number — a develop build reports rtk 0.49.0 and so does a release that lacks the subcommand
  • Passes OpenCode 2.x's event.agent as rtk hook opencode --agent <name>, so an agent.<name>.permission ask or deny is judged against the agent that runs the command. OpenCode 1.x sends no agent field, so that path stays root-only
  • Two separate off switches, both named RTK_DISABLED. The per-command form is RTK_DISABLED=1 git status, an env prefix inside the command string; the Rust rewrite engine handles that one and the plugin passes the string through untouched. The plugin also reads RTK_DISABLED=1 from OpenCode's own process environment, which switches rewriting off for the whole session
  • Mutates command in-place (event.input.command in v2, output.args.command in v1) if the answered rewrite differs from the original
  • Any failure — non-zero exit, timeout, non-JSON stdout, missing binary — passes the command through unchanged
  • Installed to ~/.config/opencode/plugins/rtk.ts by rtk init -g --opencode
  • Tests: node --test hooks/opencode/rtk.test.mjs

Overview (`hooks/pi/README.md`)

Pi Hooks

Part of hooks/

Design Intent

RTK's Pi extension is a rewrite-only token optimizer. It mutates bash commands to their
rtk-prefixed equivalents, so filtered output reaches the context.

Permission gating is intentionally out of scope. RTK does not block, confirm, or audit
commands — that concern belongs to a dedicated permission extension (e.g. one that gates
rm -rf, sudo, etc.). This separation keeps RTK's hook fast, predictable, and composable
with other Pi extensions.

Specifics

  • TypeScript extension using Pi's ExtensionAPI (not a shell hook, no zx dependency)
  • Subscribes to tool_call event, narrows to bash tool
  • Calls rtk rewrite via pi.exec; mutates event.input.command in-place if rewrite differs
  • All error paths return undefined (pass through); RTK never blocks execution on its own
  • If an rtk pipeline rule blocks the command, the extension returns { block: true, reason } with the rule's message
  • Version guard at load time: checks rtk >= 0.23.0; warns and registers no-op if too old or missing
  • Install updates only current or known historical stock content. For modified or unrelated content it asks before overwriting; --auto-patch approves and --no-patch leaves the file unchanged. A declined protected update, including --no-patch, exits nonzero; in --dry-run mode it reports the prompt without changing files or creating directories. LF and CRLF line endings are treated as the same stock content.
  • Installed to .pi/extensions/rtk.ts by rtk init --agent pi (project-local) or ~/.pi/agent/extensions/rtk.ts by rtk init --agent pi --global

The same extension is shared with Oh My Pi (OMP) through OMP's legacy-pi-compat layer. OMP installs it at .omp/extensions/rtk.ts (project-local) or ~/.omp/agent/extensions/rtk.ts (global). The global OMP path follows PI_CODING_AGENT_DIR, which OMP also honors. When the Pi and OMP paths in either scope resolve to the same file, RTK records Pi/OMP ownership in an adjacent hidden .rtk-agents state file. A valid sidecar is authoritative; missing or unreadable state is treated as uncertain, warned about, and not used to claim sole ownership. A definitively shared project or global file prompts before removal (--auto-patch approves it); an uncertain legacy or corrupt-state uninstall warns and proceeds, while --no-patch still leaves definitively shared files in place with a nonzero exit. RTK currently targets OMP's default profile and .omp project directory, not named profiles or custom PI_CONFIG_DIR locations.

Uninstall

bash
# Remove project-local install (run from the project root)
rtk init --uninstall --agent pi
# → removes .pi/extensions/rtk.ts

# Remove global install
rtk init --uninstall --agent pi --global
# → removes ~/.pi/agent/extensions/rtk.ts

Uninstalling an absent extension is a no-op. Current and known historical stock files are removed, while modified RTK content is preserved and causes a normal uninstall to fail with a manual-removal message; --dry-run previews that refusal. Unreadable content is left alone but causes a normal uninstall to fail nonzero; --dry-run reports it and succeeds. Unrelated content is left alone. When Pi and OMP paths in either scope resolve to the same file, a valid sidecar recording both agents makes uninstall ask before removing it; --auto-patch approves and --no-patch leaves it in place with a nonzero exit. Missing or unreadable ownership state produces a warning and proceeds without definitive shared-file protection. Only the extension file and RTK's adjacent ownership state are managed by install/uninstall.


Overview (`hooks/vibe/README.md`)

Mistral Vibe Hooks

Part of hooks/

Specifics

  • Uses the rtk hook vibe Rust binary (not a shell script) -- no jq dependency
  • pre_tool hook declared in ~/.vibe/hooks.toml (user-scoped) with match = "bash" and strict = false
  • Reads Vibe's stdin JSON payload (tool_name, tool_input.command, hook_event_name, session_id)
  • Returns hook_specific_output.tool_input.command for transparent rewrite plus a system_message for UI visibility
  • Non-bash tool / empty command / malformed JSON / RTK-unknown command → passthrough (exit 0, empty stdout)
  • RTK permission deny → {"decision":"deny","reason":"..."}
  • Alongside the hook, a system prompt at ~/.vibe/prompts/rtk.md is installed as a belt-and-suspenders fallback (skip with --hook-only); its content follows awareness.level in config.toml (../rtk-awareness*.md)
  • Installed globally via rtk init -g --agent vibe; there is no project-scoped variant

Notes


Overview (`hooks/windsurf/README.md`)

Windsurf (Cascade) Hooks

Part of hooks/

Specifics

  • Prompt-level guidance only (no programmatic hook) -- relies on Windsurf Cascade reading rules files
  • Installs ../rtk-awareness-full.md (shared, agent-neutral): the instruction to prefix every shell command with rtk, plus meta commands. No hook, so the full level is always used regardless of awareness.level
  • Installed to .windsurfrules (project-local, workspace-scoped) by rtk init

Overview (`src/analytics/README.md`)

Analytics

See also docs/contributing/TECHNICAL.md for the full architecture overview

Scope

Read-only dashboards over the tracking database. Queries the recorded bash output reduction, correlates with external spending data, and surfaces adoption metrics. Never modifies the tracking DB.

Owns: rtk gain (savings dashboard), rtk cc-economics (cost reduction), rtk session (adoption analysis), and Claude Code usage data parsing.

Does not own: recording token savings (that's core/tracking called by cmds/), or command filtering itself (that's cmds/).

Boundary rule: if a new module writes to the DB, it belongs in core/ or cmds/, not here. Tool-specific analytics (like cc_economics reading Claude Code data) are fine — the boundary is "read-only presentation", not "tool-agnostic".

Purpose

Bash output reduction analytics, economic modeling, and adoption metrics. The stored percentages measure output bytes; token counts are bytes / 4 estimates, not billed tokens.

These modules read from the SQLite tracking database to produce dashboards, spending estimates, and session-level adoption reports.

Adding New Functionality

To add a new analytics view: (1) create a new *_cmd.rs file in this directory, (2) query core/tracking for the metrics you need using the existing TrackingDb API, (3) register the command in main.rs under the Commands enum, and (4) add #[cfg(test)] unit tests with sample tracking data. Analytics modules should be read-only against the tracking database and never modify it.


Overview (`src/cmds/README.md`)

Command Filter Modules

Scope

Command execution and output filtering. Every module here calls an external CLI tool (Command::new("some_tool")), transforms its stdout/stderr to reduce the bytes the agent reads, and records the reduction via core/tracking.

Owns: all command-specific filter logic, organized by ecosystem (git, rust, js, python, go, dotnet, cloud, system). Cross-ecosystem routing (e.g., lint_cmd detecting Python and delegating to ruff_cmd) is an intra-component concern.

Does not own: the TOML DSL filter engine (that's core/toml_filter), hook interception (that's hooks/), or analytics dashboards (that's analytics/). This component writes to the tracking DB; analytics reads from it.

Boundary rule: a module belongs here if and only if it executes an external command and filters its output. Infrastructure that serves multiple modules without calling external commands belongs in core/.

When to Write a Rust Module (vs TOML Filter)

Rust modules exist here because they need capabilities TOML filters don't have: parsing structured output (JSON, NDJSON), state machine parsing across phases, injecting CLI flags (--format json), cross-command routing, or flag-aware filtering — detecting user-requested verbose flags (e.g., --nocapture) and adjusting compression accordingly (see Design Philosophy and TOML vs Rust decision table).

Ecosystem placement: Match the command's language/toolchain. Use system/ for language-agnostic commands. New ecosystem when 3+ related commands justify it.

For the full contribution checklist (including discover/rules.rs registration), see Adding a New Command Filter below.

Purpose

All command-specific filter modules that execute CLI commands and transform their output to minimize LLM token consumption. Each module follows a consistent pattern: execute the underlying command, filter its output through specialized parsers, track token savings, and propagate exit codes.

Ecosystems

Each subdirectory has its own README with file descriptions, parsing strategies, and cross-command dependencies.

  • git/ — git, gh, gt, diff — trailing_var_arg parsing, gh markdown filtering, gt passthrough
  • rust/ — cargo, runner (err/test) — Cargo sub-enum routing, runner dual-mode
  • js/ — npm, pnpm, vitest, lint, tsc, next, prettier, playwright, prisma — Package manager auto-detection, lint routing, cross-deps with python
  • python/ — ruff, pytest, mypy, pip — JSON check vs text format, state machine parsing, uv auto-detection
  • go/ — go test/build/vet, golangci-lint — NDJSON streaming, Go sub-enum pattern
  • dotnet/ — dotnet, binlog, trx, format_report — DotnetCommands sub-enum, internal helper modules
  • cloud/ — aws, docker/kubectl, curl, wget, psql — Docker/Kubectl sub-enums, JSON forced output
  • system/ — ls, tree, read, grep, find, wc, env, json, log, deps, summary, format, smart — format_cmd routing, filter levels, language detection
  • ruby/ — rake/rails test, rspec, rubocop — JSON injection pattern, ruby_exec() bundle exec auto-detection

Execution Flow

The shared wrappers in core/runner.rs encapsulate the execution skeleton. Modules build the Command (custom arg logic), then delegate to a runner entry point. All runners handle tracking, tee recovery, and exit code propagation automatically.

text
run_streaming()       Filter applied              tee_and_hint()
      |                (per-line or post-hoc)            |
      v                       |                          v
 +---------+  stdout  +-------+-------+  filtered  +-------+
 | Spawn   |--------->| filter        |----------->| Print |
 +---------+  stderr  +---------------+            +-------+
      |        (live)                                    |
      v                                                  v
 +----------+                                    +---------+
 | raw =    |                                    | Track   |
 | stdout + |                                    | savings |
 | stderr   |                                    +---------+
 +----------+                                          |
                                                       v
                                                 +-----------+
                                                 | Ok(code)  |
                                                 | returned  |
                                                 +-----------+

Filter modes

All execution goes through core::stream::run_streaming() with one of four FilterMode variants. The runner entry points (run_filtered, run_streamed, run_passthrough) select the appropriate mode automatically — module authors don't interact with FilterMode directly.

FilterMode How it works Used by
CaptureOnly Buffers all stdout silently, then passes the full string to filter_fn post-hoc. Stderr streams to terminal in real time. run_filtered() (default path)
Buffered Buffers all stdout, applies filter, then prints the result. Stderr streams live. Chosen automatically by run_filtered() when filter_stdout_only is set. run_filtered() (stdout-only path)
Streaming Feeds each stdout line to a StreamFilter::feed_line() as it arrives. Emitted lines print immediately. Calls flush() after process exits for final output. run_streamed()
Passthrough Inherits the parent TTY directly — no piping, no buffering. raw/filtered are empty. run_passthrough()

When to use which

Scenario Runner FilterMode Why
Parse structured output (JSON, tables) run_filtered() CaptureOnly/Buffered Filter needs full text to parse structure
Long-running, line-parseable output run_streamed() Streaming Low memory, real-time output
No filtering, just track usage run_passthrough() Passthrough Zero overhead, inherits TTY
Custom logic (multi-command, file I/O) Manual with exec_capture() CaptureOnly Full control over execution

Phases

  1. Spawn — run_streaming() starts the child process with piped stdout/stderr (or inherited TTY for Passthrough)
  2. Filter — stdout is processed per the FilterMode; stderr is forwarded to the terminal in real time via a dedicated reader thread
  3. Print — filtered output is written to stdout (live for Streaming, post-hoc for CaptureOnly/Buffered); if tee enabled, appends recovery hint on failure
  4. Track — timer.track() records raw vs filtered for token savings
  5. Exit code — returns Ok(exit_code) to caller; main.rs calls process::exit(code) once

RunOptions builder:

Constructor Behavior
RunOptions::default() Combined stdout+stderr to filter, no tee
RunOptions::with_tee("label") Combined filtering + tee recovery
RunOptions::stdout_only() Stdout-only to filter, stderr passthrough, no tee
RunOptions::stdout_only().tee("label") Stdout-only + tee recovery

Example — filtered command (recommended):

rust
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
    let mut cmd = resolved_command("mycmd");
    for arg in args { cmd.arg(arg); }
    if verbose > 0 { eprintln!("Running: mycmd {}", args.join(" ")); }

    runner::run_filtered(
        cmd, "mycmd", &display_args(args),
        filter_mycmd_output,
        runner::RunOptions::stdout_only().tee("mycmd"),
    )
}

Exit code handling is fully automatic when using run_filtered() — the wrapper extracts the exit code (including Unix signal handling via 128+signal), tracks savings, and returns Ok(exit_code). Module authors just return the result.

Streaming filters (line-by-line):

Use runner::run_streamed() when the command is long-running or produces unbounded output that should be filtered line-by-line. Three levels of abstraction, from simplest to most flexible:

Level 1: RegexBlockFilter — regex start pattern + indent continuation (3-5 lines)

For block-based errors where blocks start with a regex match and continue on indented lines. Handles skip prefixes, block counting, and summary automatically.

rust
use crate::core::shell::display_args;
use crate::core::stream::{BlockStreamFilter, RegexBlockFilter};

pub fn run(args: &[String], verbose: u8) -> Result<i32> {
    let mut cmd = resolved_command("mycmd");
    for arg in args { cmd.arg(arg); }

    let filter = RegexBlockFilter::new("mycmd", r"^error\[")
        .skip_prefixes(&["warning:", "note:"]);

    runner::run_streamed(
        cmd, "mycmd", &display_args(args),
        Box::new(BlockStreamFilter::new(filter)),
        runner::RunOptions::with_tee("mycmd"),
    )
}

RegexBlockFilter provides: regex-based block start detection, indent-based continuation (space/tab), configurable line skipping via prefixes, and automatic summary ("mycmd: 3 blocks in output" or "mycmd: no errors found").

Level 2: BlockHandler trait — custom block detection with state tracking

When you need custom block start/continuation logic or stateful parsing beyond regex + indent. Implement the BlockHandler trait and wrap in BlockStreamFilter.

rust
use crate::core::stream::{BlockHandler, BlockStreamFilter};

struct MyHandler { error_count: usize }

impl BlockHandler for MyHandler {
    fn should_skip(&mut self, line: &str) -> bool { line.is_empty() }
    fn is_block_start(&mut self, line: &str) -> bool {
        if line.starts_with("FAIL") { self.error_count += 1; true } else { false }
    }
    fn is_block_continuation(&mut self, line: &str, _block: &[String]) -> bool {
        line.starts_with("  ") || line.starts_with("at ")
    }
    fn format_summary(&self, _exit_code: i32, _raw: &str) -> Option<String> {
        Some(format!("{} failures\n", self.error_count))
    }
}

See cmds/rust/cargo_cmd.rs::CargoBuildHandler and cmds/js/tsc_cmd.rs::TscHandler for production examples.

Level 3: StreamFilter trait — full line-by-line control

When block-based parsing doesn't fit (e.g., state machines, multi-phase output, line transforms). Implement StreamFilter directly.

rust
use crate::core::stream::StreamFilter;

struct MyFilter { state: State }

impl StreamFilter for MyFilter {
    fn feed_line(&mut self, line: &str) -> Option<String> {
        // Return Some(text) to emit, None to suppress
        if line.contains("error") { Some(format!("{}\n", line)) } else { None }
    }
    fn flush(&mut self) -> String { String::new() }
    fn on_exit(&mut self, exit_code: i32, raw: &str) -> Option<String> { None }
}

See cmds/rust/runner.rs::ErrorStreamFilter for a complete reference implementation (state machine that tracks error blocks across lines).

Example — passthrough command (no filtering):

rust
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
    runner::run_passthrough("mycmd", args, verbose)
}

Example — manual execution (custom logic):

rust
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
    let output = resolved_command("mycmd").args(args)
        .output().context("Failed to run mycmd")?;
    let exit_code = exit_code_from_output(&output, "mycmd");
    // ... custom filtering, tracking ...
    Ok(exit_code)
}

Modules with deviations (subcommand dispatch, parser trait systems, two-command fallback, synthetic output).

Cross-Command Dependencies

  • lint_cmd routes to mypy_cmd or ruff_cmd when detecting Python projects
  • format_cmd routes to prettier_cmd or ruff_cmd depending on the formatter detected
  • gh_cmd imports compact_diff() from git for diff formatting (markdown helpers are defined in gh_cmd itself)

Cross-Cutting Behavior Contracts

These behaviors must be uniform across all command modules. Full audit details in docs/ISO_ANALYZE.md.

Exit Code Propagation

All module run() functions return Result where the i32 is the underlying command's exit code. main.rs calls std::process::exit(code) once at the single exit point — modules never call process::exit() directly.

Return value Meaning Who exits
Ok(0) Command succeeded main.rs exits 0
Ok(N) Command failed with code N main.rs exits N
Err(e) RTK itself failed (not the command) main.rs prints error, exits 1

How exit codes are extracted:

Execution style Helper Signal handling
cmd.output() (filtered) exit_code_from_output(&output, "tool") 128+signal on Unix
cmd.status() (passthrough) exit_code_from_status(&status, "tool") 128+signal on Unix
run_filtered() (wrapper) Automatic — no manual code needed Built-in

When using run_filtered(): exit code handling is fully automatic. The wrapper extracts the exit code, handles signals, and returns Ok(exit_code). Module authors just return the wrapper's result — no exit code logic needed.

When doing manual execution: use exit_code_from_output() or exit_code_from_status() and return Ok(exit_code). Never call process::exit(), never use .code().unwrap_or(1) (loses signal info).

Filter Failure Passthrough

When filtering fails, fall back to raw output and warn on stderr. Never block the user.

Tee Recovery

Modules that parse structured output (JSON, NDJSON, state machines) must call tee::tee_and_hint() so users can recover full output on failure.

Internal Truncation Recovery

When a filter caps a list at N items (e.g. take(20)), the remaining items must be accessible via a tee hint. Never show "… +N more" without a recovery path — the agent has no way to retrieve the hidden content.

Choosing the right hint:

Content type Function Condition
Flat list — one item = one line in the tee force_tee_tail_hint(content, slug, MAX + 1) PR lists, error lines, file paths — anything where each item is a single-line string
Multi-line blocks force_tee_hint(content, slug) Test failures, build error blocks — items that span multiple lines so a line offset is meaningless

Cap values come from src/core/truncate.rs. Pick the CAP_* matching your data class (CAP_ERRORS, CAP_WARNINGS, CAP_LIST, CAP_INVENTORY) and bind it to a local const MAX_XXX: usize = CAP_Y;. Derive take(MAX_XXX), > MAX_XXX, and the offset MAX_XXX + 1 from the local. These CAPs will later become the configuration surface for per-filter cap tuning (user-overridable via config) — keep all truncation values routed through them so that hook lands as a single switch rather than a codebase-wide hunt. A filter that genuinely needs to deviate uses truncate::reduced(CAP_Y, n) (e.g. reduced(CAP_WARNINGS, 5)) so it still tracks the global when reconfigured — never a bare literal, never cap - n (underflows once caps are runtime-configurable), and never *// (those scale unboundedly). reduced falls back to the full cap if the reduction would empty the list. Each deviation needs a one-line comment stating why; if there's no real reason, just use the plain CAP. See src/core/README.md ("Truncation Caps") for the full rationale.

The stored content must match what was shown. For force_tee_tail_hint, build the recall blob from the same formatted values displayed — not raw/intermediate data — and pass the 1-based first-hidden line as offset. rtk recall <hash> slices the stored content from that offset, so the agent gets exactly the hidden items. If the filter reformats items before displaying them, pre-build a Vec<String> of formatted lines and use it for both the display loop and the recall blob.

Stderr Handling

Modules must capture stderr and include it in the raw string passed to timer.track(), so token savings reflect total output.

Tracking Completeness

All modules must call timer.track() on every path — success, failure, and fallback. Since modules return Ok(exit_code) instead of calling process::exit(), tracking always runs before the program exits.

Verbose Flag

All modules accept verbose: u8. Use it to print debug info (command being run, savings %, filter tier). Do not accept and ignore it.

Adding a New Command Filter

Adding a new filter or command requires changes in multiple places. For TOML-vs-Rust decision criteria, see CONTRIBUTING.md.

Rust module (structured output, flag injection, state machines)

  1. Create module in src/cmds/<ecosystem>/mycmd_cmd.rs:
    • Write the filter_mycmd() function (pure: &str -> String, no side effects)
    • Write pub fn run(...) -> Result using runner::run_filtered() — build the Command, choose RunOptions, delegate
    • Use RunOptions::stdout_only() when the filter parses structured stdout (JSON, NDJSON) — stderr would corrupt parsing
    • Use RunOptions::default() when filtering combined text output
    • Add .tee("label") when the filter parses structured output (enables raw output recovery on failure)
    • Exit codes: handled automatically by run_filtered() — just return its result
    • Truncation: if the filter caps any list at N items, emit force_tee_tail_hint (flat lists) or force_tee_hint (multi-line blocks) so the agent can recover hidden items — see Internal Truncation Recovery. Use a named constant for the cap; derive the offset from it (MAX_XXX + 1)
    • Arguments: if the filter inspects its args at all — which flags were passed, which are paths, what to inject or strip — classify them with arg_tokenizer, never a string scan. Write the takes_value predicate from the tool's own --help and check it against the real binary
  2. Register module:
    • Add pub mod mycmd_cmd; to the ecosystem mod.rs, keeping the list alphabetical. Modules are listed explicitly rather than generated from the directory: rustfmt only follows literal mod items and cannot expand macros, so a generated list hides every file under src/cmds/ from cargo fmt — and therefore from CI's fmt gate. build.rs fails the build if a .rs file here is missing from its mod.rs, since an undeclared module is never compiled, linted, or tested.
    • Add variant to Commands enum in main.rs with #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    • Add routing match arm in main.rs: Commands::Mycmd { args } => mycmd_cmd::run(&args, cli.verbose)?,
  3. Add rewrite pattern — Entry in src/discover/rules.rs (PATTERNS + RULES arrays at matching index) so hooks auto-rewrite the command
  4. Write tests — Real fixture, snapshot test, >= 20% reduction in bash output (measured with RTK's token estimator, see testing rules)
  5. Update docs — Ecosystem README (CHANGELOG.md is auto-generated by release-please)

TOML filter (simple line-based filtering)

  1. Create filter in src/filters/
  2. Add rewrite pattern in src/discover/rules.rs
  3. Write tests and update docs

Overview (`src/core/README.md`)

Core Infrastructure

See also docs/contributing/TECHNICAL.md for the full architecture overview

Scope

Domain-agnostic building blocks with no knowledge of any specific command, hook, or agent. If a module references "git", "cargo", "claude", or any external tool by name, it does not belong here. Core is a leaf in the dependency graph — it is consumed by all other components but imports from none of them.

Owns: configuration loading, token tracking persistence, TOML filter engine, tee output recovery, display formatting, explicit shell/direct command construction, telemetry, and shared utilities.

Does not own: command-specific filtering logic (that's cmds/), hook lifecycle management (that's src/hooks/), or analytics dashboards (that's analytics/).

Purpose

Core infrastructure shared by all RTK command modules. Every filter, tracker, and command handler depends on these modules. No inward dependencies — leaf in the dependency graph (no circular imports possible).

TOML Filter Pipeline

The TOML DSL applies 8 stages in order:

  1. strip_ansi: Remove ANSI escape codes if enabled
  2. replace: Line-by-line regex substitutions (chainable, supports backreferences)
  3. match_output: Short-circuit rules (if output matches pattern, return message; unless field prevents swallowing errors)
  4. strip/keep_lines: Filter lines by regex (mutually exclusive)
  5. truncate_lines_at: Truncate each line to N chars (unicode-safe)
  6. head/tail_lines: Keep first N or last N lines (with omit message)
  7. max_lines: Absolute line cap applied after head/tail
  8. on_empty: Return message if result is empty after all stages

Three-tier filter lookup (first match wins):

  1. .rtk/filters.toml (project-local, requires rtk trust)
  2. ~/.config/rtk/filters.toml (user-global)
  3. Built-in filters concatenated by build.rs at compile time

Source File Comment Stripping

src/core/filter.rs is a separate engine from the TOML DSL: it filters source
files
(used by rtk read) rather than command output. At -l minimal it
strips comments using the per-language delimiters in
Language::comment_patterns().

Python does not use that walk. It has no block comments — """ opens a
string, which may be a docstring or an ordinary value — so it gets a
string-aware path that removes # comments and leaves string contents alone.
Matching """ as a block delimiter misread both of these:

python
QUERY = """          # contains """ without starting with it
SELECT 1
"""

"""Module doc."""    # opens and closes on one line

Docstrings are kept at minimal. aggressive has no string awareness: it
keeps a line inside a string when that line looks like an import or a
signature.

Tracking Database Schema

sql
CREATE TABLE commands (
  id INTEGER PRIMARY KEY,
  timestamp TEXT,              -- UTC ISO8601
  original_cmd TEXT,           -- "ls -la"; words quoted only when needed: "grep 'a b' f"
  rtk_cmd TEXT,                -- "rtk ls", "rtk:toml make all", "rtk:passthrough git tag"
  project_path TEXT,           -- cwd (for project-scoped stats)
  input_tokens INTEGER,        -- estimated from raw output (bytes / 4, no tokenizer)
  output_tokens INTEGER,       -- estimated from filtered output (bytes / 4)
  saved_tokens INTEGER,        -- input - output
  savings_pct REAL,            -- (saved / input) * 100, i.e. reduction in bash output bytes
  exec_time_ms INTEGER         -- elapsed milliseconds
);

CREATE TABLE parse_failures (
  id INTEGER PRIMARY KEY,
  timestamp TEXT,
  raw_command TEXT,
  error_message TEXT,
  fallback_succeeded INTEGER   -- 1=yes, 0=no
);

Project-scoped queries use GLOB patterns (not LIKE) to avoid _/% wildcard issues in paths.

Config Sections

toml
[tracking]
enabled = true
history_days = 90
database_path = "/custom/path/to/tracking.db"  # Optional

[display]
colors = true
emoji = true
max_width = 120

[retriever]
mode = "sqlite"             # sqlite (default) | tee (legacy files) | disabled
max_entry_bytes = 10485760  # sqlite: 10 MiB per entry
max_entries = 200           # sqlite: FIFO cap
retention_days = 30         # sqlite: 0 disables age eviction
compression = true          # sqlite: gzip blobs (lossless)
# database_path = "/custom/recall.db"
tee_max_files = 20          # tee mode: rotation
tee_max_file_size = 1048576 # tee mode: per-file cap
# tee_directory = "/custom/tee/dir"

[telemetry]
enabled = true

[hooks]
exclude_commands = ["curl", "playwright"]  # Never auto-rewrite these

[limits]
grep_max_results = 200
grep_max_per_file = 25
status_max_files = 15
status_max_untracked = 10
passthrough_max_chars = 2000

Shared Utilities (utils.rs)

Key functions available to all command modules:

Function Purpose
truncate(s, max) Truncate string with ... suffix
strip_ansi(text) Remove ANSI escape/color codes
resolved_command(name) Find command in PATH, returns Command
tool_exists(name) Check if a CLI tool is available
detect_package_manager() Detect pnpm/yarn/npm from lockfiles
package_manager_exec(tool) Build Command using detected package manager
ruby_exec(tool) Auto-detect bundle exec when Gemfile exists
count_tokens(text) Estimate tokens: ceil(chars / 4.0)

Argument Tokenizer (arg_tokenizer.rs)

Shared classifier for an already----restored args slice (see args_utils::restore_double_dash) into flags, their values, and positionals. tokenize_grammar takes a takes_value(kind, name) predicate the caller supplies — the list of which flags take a value is per-tool, but the token-walking around it isn't. tokenize(args) is the structural-only entry point, for a caller asking which arguments are flags and where -- is; it assumes no flag takes a value, so anything reading a value or counting free positionals needs tokenize_grammar.

Use it for anything that decides what an argument is. A new command filter, a new flag on an existing one, or a fix to how one is detected goes through tokenize_grammar — not starts_with('-'), not args.iter().any(|a| a == "--flag"). Those miss exactly what this module exists for: a flag's own value (git log --grep -p searches for the string "-p"), an attached value (--flag=v, /bl:x), a short cluster (-rn), and everything past --. Every bug the migration fixed was one of those four.

Four rules, each of which cost a real bug before it was written down:

  • One grammar per tool and subcommand. Never reuse a sibling's predicate wholesale because it looks close enough — -u means -p in git log but --include-untracked in git stash show, and -T is --initial-tab in grep but --type-not in rg. Transcribe from the tool's own --help and verify against the real binary; grep and rg share 13 of ~50 value-taking flags, so one merged table is wrong for both.
  • Scope the lookup to the region the tool parses. Everything past -- is a pathspec or an argument forwarded to another program — before_dashdash gives the tool's own tokens. Dialect::Msbuild keeps classifying past the boundary (it forwards rather than ending option parsing), which makes this explicit slice mandatory there, not optional.
  • Inject before the boundary. RTK's own flags go at injection_point, never appended: dotnet parks anything after -- in UnparsedTokens and git reads it as a pathspec, so an appended --verify-no-changes silently does nothing.
  • Detect and act with one rule. Strip or inject using the detected token's source_index; re-matching the text lets the two disagree, which deleted a pathspec named --no-compact and swallowed a forwarded --write.

takes_value returns Option<ValueSpec>, not a bool: one table per tool, answering every question the tokenizer has about a flag's value rather than one predicate per question, which is how two lists start drifting apart.

  • ValueSpec::value() — --flag=v or --flag v, and a literal -- is the boundary. The common case.
  • ValueSpec::attached_only() — --flag=v only, the next argument is never the value (git's -M/-U/-C/-B take an optional attached number and nothing else).
  • ValueSpec::solo_only() — a Short flag takes a separate value only when it is the whole argument: git log -n 2 does, git log -pn 2 does not. No meaning for a Long flag.
  • .claiming_dash_dash() — lets a literal -- be this flag's value. A per-tool split, not per-flag: grep and rg let any value-taking flag swallow it, git and cargo reject it whichever flag is asking.

Dialects

Everything that is not per-flag is per-tool, and stays a parameter: tokenize_grammar(args, takes_value, Dialect::Msbuild). Dialect is a Copy struct of five independent axes, because the tools measured want five different combinations of them:

axis values what differs
single_dash Cluster / Atomic / AtomicAliasingLong what -abc is: three short flags, one flag name, or one flag name that is also --abc
attach Equals / EqualsOrColon which separator attaches a value (--logger:trx)
dash_dash EndsOptions / Forwards / EndsGlobalOptions whether classification stops at --, continues because the tail is forwarded to another program, or continues because only the global option region ended
name_case Sensitive / Folded whether flag lookups fold ASCII case
slash_flags bool whether /flag is a switch rather than a path

Naming rule. A preset names a grammar family that several tools can share — a parser library, or a real convention — so its name answers "can my tool reuse this?". Dialect::CommonsCli is checkable (ls /usr/share/maven/lib/ ships commons-cli-1.11.0.jar); "is my tool Maven?" is not. A single tool's bespoke parser gets no preset: its caller composes the axes at its own call site. That rule is what stops this list growing one variant per tool.

  • Dialect::Posix — git, cargo, rg, golangci-lint. Cluster, =, -- ends options, case-sensitive, no /flag.
  • Dialect::Msbuild — dotnet. Atomic, = or :, -- forwards, case-folded, /flag.
  • Dialect::CommonsCli — Apache commons-cli. POSIX with Atomic: the library's short options are whole multi-character words (-pl, -am, -gs, -emp), so mvn -Bo is an error, not a cluster. Maven is the first consumer.
  • Dialect::GoFlag — Go's flag package: atomic single-dash options, and -run is the same flag as --run.

Gradle gets no preset: its parser is org.gradle.cli (gradle:jdk21 ships gradle-cli-*.jar and no commons-cli), used by nothing else. It is Posix with dash_dash: EndsGlobalOptions, composed as a const in gradlew_cmd.rs. The axis value is shared infrastructure; the one-tool combination is not.

src/core/arg_tokenizer/frozen.rs is the pre-axes implementation, kept as the oracle for the differential test in differential.rs: every arg vector up to four tokens over an alphabet covering each construct the scanner branches on, asserted token-for-token identical under Posix and Msbuild. Never edit frozen.rs to match new behaviour — a diff against it is the only proof the presets have not moved.

Consumer Contracts

Core provides infrastructure that cmds/ and other components consume. These contracts define expected usage.

Command Construction (`shell`)

Use shell::direct_command() when the caller already has an argv vector. It
preserves argument boundaries and never expands globs, variables, redirects,
or operators. Use shell::shell_command() only for an intentional command
string, with an explicit shell when syntax is shell-specific. The platform
default remains sh -c on Unix and cmd /C on Windows for compatibility.

Never infer the command parser from $SHELL: agent hosts and terminal wrappers
can execute a different shell while preserving the user's login-shell value.
Callers that accept --shell must require the complete script as one quoted
argument instead of reconstructing it by joining parsed argv.

Tracking (`TimedExecution`)

Consumers must call timer.track() on all code paths — success, failure, and fallback. Calling std::process::exit() before track() loses metrics. The raw string passed to track() should include both stdout and stderr to produce accurate savings percentages.

Output recovery (`tee_and_hint` + recall store)

Consumers that parse structured output (JSON, NDJSON, state machines) should call tee::tee_and_hint() to persist raw output for LLM recovery on failure. It must be called before std::process::exit().

For truncation recovery on success (e.g. a list capped at 20 items), use tee::force_tee_hint() (multi-line blocks) or tee::force_tee_tail_hint(content, slug, offset) (flat lists). All three persist the full output to the content-addressed recall store (retriever.rs) and emit a runnable hint — [full output: rtk recall <hash>] or [+N hidden: rtk recall <hash>] — instead of burning tokens working around missing data.

The agent runs rtk recall <hash> to get back exactly what was elided. For force_tee_tail_hint, offset is the 1-based first hidden line (header_lines + MAX_CAP + 1); it is stored so the default recall returns only the hidden tail. Storage is byte-faithful (BLOB + lossless gzip); tune limits via the [retriever] config section.

Truncation Caps (`truncate`)

src/core/truncate.rs defines four global cap policies — CAP_ERRORS, CAP_WARNINGS, CAP_LIST, CAP_INVENTORY — for the data classes RTK filters truncate. Each filter binds the right CAP to a local const MAX_* so the cap is one named jump away from the call site. These CAPs are the staging point for filter-level cap configuration (planned, not yet implemented): once the config surface lands, overriding CAP_LIST in ~/.config/rtk/config.toml will tune every list filter in one place instead of editing 20+ files.

Config policy. Configured values are accepted as-is, including 0, which means "summary only" — the filter still prints the count and the [full output: …] recovery hint, just no individual items. Caps are never refused and rtk never aborts on them, in keeping with the never-block-the-user fallback philosophy.

Deviating from a cap. A filter whose items are unusually verbose (multi-line entries, backtraces) may show fewer than its class cap. Use truncate::reduced(cap, by) rather than a bare cap - by: reduced returns cap - by, except when the reduction would empty the list (by >= cap), in which case it drops the deviation and uses the full cap. This guarantees a deviation can never hide every item, and — crucially — stays a usize-underflow-safe const fn once caps become runtime-configurable (a bare CAP_WARNINGS - 5 would panic or wrap to "no truncation" if a user set CAP_WARNINGS below 5). Never deviate with a bare literal or with *// (those scale unboundedly). Each deviation needs a one-line comment stating why.

Adding New Functionality

Place new infrastructure code here if it meets all of these criteria: (1) it has no dependencies on command modules or hooks, (2) it is used by two or more other modules, and (3) it provides a general-purpose utility rather than command-specific logic. Follow the existing pattern of lazy-initialized resources (LazyLock for regex, on-demand config loading) to preserve the <10ms startup target. Add #[cfg(test)] mod tests with unit tests in the same file.


Overview (`src/discover/README.md`)

Discover — History Analysis & Command Rewrite

Full rewrite pipeline diagram: docs/contributing/TECHNICAL.md

What This Module Does

This module has two jobs:

  1. Rewrite commands — Every LLM agent hook calls rtk rewrite "git status". This module decides whether to rewrite it (rtk git status) or pass it through unchanged. This is the hot path — every command the LLM runs goes through here.

  2. Analyze history — rtk discover scans past LLM sessions to find commands that could have been rewritten but weren't. Same classification logic, different consumer.

How Command Rewriting Works

When a hook sends cargo fmt --all && cargo test 2>&1 | tail -20:

Tokenization — The lexer (lexer.rs) turns the raw string into typed tokens. It's a single-pass state machine that understands shell quoting, escapes, redirects, and operators. This is critical because naive string splitting breaks on quoted content like git commit -m "fix && update".

text
"cargo test 2>&1 && git status"
→ [Arg("cargo"), Arg("test"), Redirect("2>&1"), Operator("&&"), Arg("git"), Arg("status")]

Compound splitting — The rewrite engine walks the tokens, splitting on Operator (&&, ||, ;, and the case terminators ;;, ;&, ;;&) and typed Pipe tokens (|, |&). For normal pipelines, intermediate stages stay raw. A final stage whose rule's pipeline_safety allows it (ordinary grep and rg invocations) is rewritten; search pattern-file forms (-f/--file) defer because they can consume pipeline stdin as configuration. The producer stage is rewritten when its rule's pipeline_safety allows it and every downstream stage is a display-only consumer (cat, head, non-following tail) with no file-target redirect (#3171). Stderr pipelines (|&) and pipelines containing opaque shell groups remain raw.

Quoted shell wrappers — An exact sh -c, bash -c, zsh -c or fish -c segment whose script is one balanced quoted argument has that script rewritten in place, leaving the shell path, quote delimiters, spacing, suffix arguments and redirects byte-identical (shell_wrapper.rs). Recursion is capped at one level, so a wrapper inside a wrapper stays raw. A script carrying a newline, heredoc, $((, or any construct the lexer cannot attest defers unchanged. The wrapper names the shell, so the script is attested under that shell's grammar (ShellDialect): inside fish -c, (cmd) is substitution and and/or/end are control flow, and either one defers — while the same shapes in a host command string or a POSIX wrapper stay ordinary bash and keep rewriting.

Per-segment rewriting — Each segment goes through:

  1. Strip trailing redirects (2>&1, >/dev/null) — matched via lexer tokens, set aside, re-appended after rewriting
  2. Short-circuit special cases — head -20 file → rtk read file --head-lines 20, tail -n 5 file → rtk read file --tail-lines 5. These can't go through generic prefix replacement because it would produce rtk read -20 file (wrong flag position)
  3. Classify the command — strip env prefixes (FOO="bar baz"), normalize paths (/usr/bin/grep → grep), strip git global opts (git -C /tmp → git), then match against 60+ regex patterns from rules.rs
  4. Apply the rewrite — find the matching rule, replace the command prefix with rtk <cmd>, re-prepend the env prefix, re-append the redirect suffix

Guards along the way:

  • RTK_DISABLED=1 in the env prefix → skip rewrite
  • gh with --json/--jq/--template → skip (structured output, rtk would corrupt it)
  • cat with flags other than -n → skip (different semantics than rtk read)
  • cat/head/tail with > or >> → skip (write operation, not a read)
  • Command in hooks.exclude_commands config → skip

Result: rtk cargo fmt --all && rtk cargo test 2>&1 | tail -20. Bash handles the && and | at execution time — each rtk invocation is a separate process.

Shared Lexer Toolkit

lexer.rs is layer 1 of RTK's command parsing (raw string → quote/operator-aware tokens and words) — layer 2, already-split argv → flags/values, is core/arg_tokenizer.rs. It's not private to this module: hooks/permissions.rs, hooks/mod.rs, and main.rs's rtk bypass all build on it instead of re-scanning commands themselves.

Function Purpose Used outside discover/ by
tokenize(cmd) Full shell-syntax tokens: quotes, escapes, operators, pipes, redirects, shellisms —
tokenize_with_newlines(cmd) Like tokenize, plus a \n Operator token per unquoted newline (a lone \r stays glued to its word, matching real bash) —
shell_split(cmd) Quote-aware split into argv-ready words (quotes stripped, escapes resolved) hooks/mod.rs::is_claude_hook_command, main.rs's rtk bypass '...'
split_for_permissions(cmd) Segments a compound command for the permission gate — deliberately the most conservative of three segmenters (see its doc comment for the full comparison table) hooks/permissions.rs::check_command_with_rules
split_on_operators(cmd, stop_at_pipe) Segments for classification only — not safe for permission/security decisions registry.rs::split_command_chain
contains_unattestable_construct(cmd) True for command/process substitution, quoting the lexer reads differently from bash ($'\''), or a file-target redirect — constructs the permission gate can't decompose and must never auto-allow hooks/permissions.rs::check_command_with_rules
contains_unattestable_construct_in(cmd, dialect) The same check for a string whose shell is known — adds fish's (cmd) substitution and command-position control keywords registry.rs::rewrite_shell_wrapper (fish -c scripts only)

The permission gate, discover/analytics classification, and rewrite each segment compound commands (&&, ;, |, background &, subshells) slightly differently on purpose — the gate must never under-segment (a hidden command could evade a deny rule), while rewrite and analytics only need to reproduce or classify the command's actual shape. Don't reuse split_on_operators or rewrite_compound's segmenting for a permission/security decision; use split_for_permissions.

How History Analysis Works

rtk discover reads Claude Code JSONL session files. Each file contains tool_use/tool_result pairs for every command the LLM ran. The module:

  1. Extracts commands from the JSONL (via SessionProvider trait — currently only Claude Code)
  2. Splits compound commands using the same lexer-based tokenization
  3. Classifies each command against the same rules used for live rewriting
  4. Aggregates results: which commands could have been rewritten, estimated token savings, adoption rate

The classification logic is shared between discover and rewrite — same patterns, same rules, different consumers.

Env Prefix Handling

The ENV_PREFIX regex strips env variable assignments and env from the front of commands. sudo is deliberately not stripped, so sudo-prefixed commands stay unclassified and pass through unrewritten. It handles:

  • Unquoted: FOO=bar
  • Double-quoted with spaces: FOO="bar baz"
  • Single-quoted: FOO='bar baz'
  • Escaped quotes: FOO="he said \"hello\""
  • Chained: A="x y" B=1 env git status

The prefix is stripped twice: once in classify_command() to match the underlying command against rules, and again in rewrite_segment() to extract it for re-prepending to the rewritten command.

Process Wrapper Handling

A process wrapper runs another command without changing which command runs, so
the rewrite peels it, rewrites what it wraps, and re-prepends the wrapper text
byte for byte: timeout 300 cargo test becomes timeout 300 rtk cargo test.
PROCESS_WRAPPERS in registry.rs describes each wrapper's own arguments —
options that take a value, options that do not, values that may be attached to
their option, and any positional argument the wrapper consumes before the
command (timeout's duration).

Two rules keep the peeling honest. An option the table does not describe drops
the rewrite, because an unknown option may consume the following word and make
the wrong token look like the command. Shell syntax before the command (a
redirect, a subshell, a glob) does the same, because the wrapper's argv can no
longer be read off the token list.

stdbuf is deliberately not a wrapper here: it exists to make the wrapped
command emit output incrementally, and routing through rtk buffers that output
until the child exits, so rewriting it would remove the only reason to type it.
The permission gate still reads past it, and past a bare xargs, when it
matches deny and ask rules (matching_readings): recognising the command
behind a prefix and rewriting through it are separate decisions.

Wrapping also changes who receives a signal. timeout 300 rtk cargo test
signals rtk rather than cargo, so core::stream relays SIGINT/SIGTERM to the
child and lets the normal filter-and-print path finish. Without that relay a
killed run prints nothing at all, which is strictly worse than the unwrapped
command.

Adding a New Rewrite Rule

Add an entry to rules.rs. Each rule has:

  • pattern — regex that matches the command (used by RegexSet for fast matching)
  • rtk_cmd — the RTK command it maps to (e.g., "rtk cargo")
  • rewrite_prefixes — command prefixes to replace (e.g., &["cargo"])
  • category, savings_pct — metadata for discover reports
  • subcmd_savings, subcmd_status — per-subcommand overrides

No other files need to change. The registry compiles the patterns at first use via LazyLock.


Overview (`src/filters/README.md`)

Built-in Filters

See also docs/contributing/TECHNICAL.md for the full architecture overview

Each .toml file in this directory defines one filter and its inline tests.
Files are concatenated alphabetically by build.rs into a single TOML blob embedded in the binary.

When to Use a TOML Filter

TOML filters strip noise lines — they don't reformat output. The filtered result must still look like real command output (see Design Philosophy). For the full TOML-vs-Rust decision criteria, see CONTRIBUTING.md.

TOML works well for commands with predictable, line-by-line text output where regex filtering achieves 60%+ savings:

  • Install/update logs (brew, composer, poetry) — strip Using ... / Already installed lines
  • System monitoring (df, ps, systemctl) — keep essential rows, drop headers/decorations
  • Simple linters (shellcheck, yamllint, hadolint) — strip context, keep findings
  • Infra tools (terraform plan, helm, rsync) — strip progress, keep summary

For the full contribution checklist (including discover/rules.rs registration), see src/cmds/README.md — Adding a New Command Filter.

Adding a filter

  1. Copy any existing .toml file and rename it (e.g. my-tool.toml)
  2. Update the three required fields: description, match_command, and at least one action field
  3. Add [[tests.my-tool]] entries to verify the filter behaves correctly
  4. Run cargo test — the build step validates TOML syntax and runs inline tests

File format

toml
[filters.my-tool]
description = "Short description of what this filter does"
match_command = "^my-tool\\b"          # regex matched against the full command string
strip_ansi = true                       # optional: strip ANSI escape codes first
strip_lines_matching = [               # optional: drop lines matching any of these regexes
  "^\\s*$",
  "^noise pattern",
]
max_lines = 40                          # optional: keep only the first N lines after filtering
on_empty = "my-tool: ok"               # optional: message to emit when output is empty after filtering

[[tests.my-tool]]
name = "descriptive test name"
input = "raw command output here"
expected = "expected filtered output"

Available filter fields

Field Type Description
description string Human-readable description
match_command regex Matches the command string (e.g. "^docker\\s+inspect")
strip_ansi bool Strip ANSI escape codes before processing
filter_stderr bool Capture and merge stderr into stdout before filtering (use for tools like liquibase that emit banners to stderr)
strip_lines_matching regex[] Drop lines matching any regex
keep_lines_matching regex[] Keep only lines matching at least one regex
replace array Regex substitutions ({ pattern, replacement })
match_output array Short-circuit rules ({ pattern, message })
truncate_lines_at int Truncate lines longer than N characters
max_lines int Keep only the first N lines
tail_lines int Keep only the last N lines (applied after other filters)
on_empty string Fallback message when filtered output is empty

Naming convention

Use the command name as the filename: terraform-plan.toml, docker-inspect.toml, mix-compile.toml.
For commands with subcommands, prefer <cmd>-<subcommand>.toml over grouping multiple filters in one file.

Build and runtime pipeline

How a .toml file goes from contributor → binary → filtered output.

mermaid
flowchart TD
    A[["src/filters/my-tool.toml\n(new file)"]] --> B

    subgraph BUILD ["cargo build"]
        B["build.rs\n1. ls src/filters/*.toml\n2. sort alphabetically\n3. concat → BUILTIN_TOML"] --> C
        C{"TOML valid?\nDuplicate names?"} -->|"fail"| D[["Build fails\nerror points to bad file"]]
        C -->|"ok"| E[["OUT_DIR/builtin_filters.toml\n(generated)"]]
        E --> F["rustc embeds via include_str!"]
        F --> G[["rtk binary\nBUILTIN_TOML embedded"]]
    end

    subgraph TESTS ["cargo test"]
        H["test_builtin_filter_count\nassert_eq!(filters.len(), N)"] -->|"wrong count"| I[["FAIL"]]
        J["test_builtin_all_filters_present\nassert!(names.contains('my-tool'))"] -->|"name missing"| K[["FAIL"]]
        L["test_builtin_all_filters_have_inline_tests\nassert!(tested.contains(name))"] -->|"no tests"| M[["FAIL"]]
    end

    subgraph RUNTIME ["rtk my-tool args"]
        R["TomlFilterRegistry::load()\n1. .rtk/filters.toml\n2. ~/.config/rtk/filters.toml\n3. BUILTIN_TOML\n4. passthrough"] --> S
        S{"match_command\nmatches?"} -->|"no match"| T[["exec raw (passthrough)"]]
        S -->|"match"| U["exec command\ncapture stdout"]
        U --> V["8-stage pipeline\nstrip_ansi → replace → match_output\n→ strip/keep_lines → truncate\n→ tail_lines → max_lines → on_empty"]
        V --> W[["print filtered output + exit code"]]
    end

    G --> H & J & L & R

Filter lookup priority

mermaid
flowchart LR
    CMD["rtk my-tool args"] --> P1
    P1{"1. .rtk/filters.toml\n(project-local)"}
    P1 -->|"match"| WIN["apply filter"]
    P1 -->|"no match"| P2
    P2{"2. ~/.config/rtk/filters.toml\n(user-global)"}
    P2 -->|"match"| WIN
    P2 -->|"no match"| P3
    P3{"3. BUILTIN_TOML\n(binary)"}
    P3 -->|"match"| WIN
    P3 -->|"no match"| P4[["exec raw (passthrough)"]]

First match wins. A project filter with the same name as a built-in shadows the built-in and triggers a warning:

text
[rtk] warning: filter 'make' is shadowing a built-in filter

Custom filters and trust

You can add your own filters in two places (both use the format above):

  • Project-local — .rtk/filters.toml (committed with a repo, applies in that project)
  • User-global — ~/.config/rtk/filters.toml (applies in every project)

Because a filter can rewrite or hide the command output an agent sees, custom filter files are not applied until you trust them. An untrusted (or edited) filter file is skipped silently on the command path — RTK never prints a warning around a rewritten command. You discover and enable untrusted filters through commands you run deliberately:

bash
rtk trust       # lists each detected filter (labelled project/global) + a risk summary, then asks to confirm ([y/N], or --yes)
rtk untrust     # revokes trust
  • Trust is recorded as a SHA-256 of the file's contents, so editing a trusted file requires re-running rtk trust — a content change invalidates trust.
  • rtk init detects existing custom filters and lets you enable them — an interactive [y/N], or --trust-filters / --no-trust-filters for scripts. It stays silent for an empty template (comments only) and, when run non-interactively, leaves filters disabled.
  • Built-in filters (this directory) are compiled into the binary and always trusted; only the on-disk project and user-global files are gated.

Honest limitation: this is consent + tamper-evidence, not a sandbox. An attacker who can write your filter file can usually also write the trust store (~/.local/share/rtk/trusted_filters.json) and bypass the gate. It defends the common case — a filter dropped in by a script, a dotfile sync, or an untrusted repo — not a same-user attacker who specifically targets RTK.


Overview (`src/hooks/README.md`)

Hook System

See also docs/contributing/TECHNICAL.md for the full architecture overview | hooks/ for deployed hook artifacts

Scope

The lifecycle management layer for LLM agent hooks: install, uninstall, verify integrity, audit usage, and manage trust. This component creates and maintains the hook artifacts that live in hooks/ (root), but does not execute rewrite logic itself — that lives in discover/registry.

Owns: rtk init installation flows (6 agents via AgentTarget enum, now including Mistral Vibe + 3 special modes: Gemini, Codex, OpenCode), SHA-256 integrity verification, hook version checking, audit log analysis, rtk rewrite CLI entry point, and TOML filter trust management.

Does not own: the deployed hook scripts themselves (that's hooks/), the rewrite pattern registry (that's discover/), or command filtering (that's cmds/).

Boundary notes:

  • decision.rs is the single place RTK decides what a hook should do with a command — deny, defer, rewrite-and-allow, or rewrite-and-ask. All three entry points route through it: the in-process rtk hook <agent> hosts (hook_cmd.rs), the rtk rewrite subprocess path (rewrite_cmd.rs), and the rtk hook check diagnostic (main.rs). Add a gate there, not in a caller.
  • rewrite_cmd.rs is a thin CLI bridge — it exists to serve hooks (hooks call rtk rewrite as a subprocess) and renders decision.rs's verdict as the exit codes those delegates branch on.
  • trust.rs gates project-local TOML filter execution. It lives here because the trust workflow is tied to hook-installed filter discovery, not to the core filter engine.

Purpose

LLM agent integration layer that installs, validates, and executes command-rewriting hooks for AI coding assistants. Hooks intercept raw CLI commands (e.g., git status) and rewrite them to RTK equivalents (e.g., rtk git status) so that LLM agents automatically benefit from token savings without explicit user configuration.

Installation Modes

rtk init supports these installation flows:

Mode Command Creates Patches
Default (global) rtk init -g Hook, SHA-256 hash, RTK.md settings.json, CLAUDE.md
Hook only rtk init -g --hook-only Hook, SHA-256 hash settings.json
Claude-MD (legacy) rtk init --claude-md 134-line RTK block CLAUDE.md
Windsurf rtk init -g --agent windsurf .windsurfrules --
Cline rtk init --agent cline .clinerules --
Codex rtk init --codex RTK.md + .codex/hooks.json (local) or $CODEX_HOME/hooks.json (global) AGENTS.md + PreToolUse hook
Cursor rtk init -g --agent cursor Cursor hook hooks.json
Trae rtk init --agent trae (project) or rtk init -g --agent trae (global) Native rtk hook trae registration .trae/hooks.json; global also patches existing ~/.trae-cn/hooks.json
Google Antigravity rtk init --agent antigravity / rtk init -g --agent antigravity Antigravity Plugin in plugins/rtk/ plugin.json, hooks.json, rules/AGENTS.md (awareness)
Pi rtk init --agent pi .pi/extensions/rtk.ts --
Oh My Pi (OMP) rtk init --agent omp .omp/extensions/rtk.ts (shared Pi extension) --
Hermes rtk init --agent hermes Python plugin in ~/.hermes/plugins/rtk-rewrite/ config.yaml plugins.enabled

Integrity Verification

The integrity system prevents unauthorized hook modifications:

  1. At install: integrity::store_hash() computes SHA-256 of the hook file, writes to ~/.claude/hooks/.rtk-hook.sha256 (read-only 0o444)
  2. At runtime: integrity::runtime_check() re-computes hash and compares; blocks execution if tampered
  3. On demand: rtk verify prints detailed verification status (PASS/FAIL/WARN/SKIP)

Five integrity states:

  • Verified: Hash matches stored value
  • Tampered: Hash mismatch (blocks execution)
  • NoBaseline: Hook exists but no hash stored (old install)
  • NotInstalled: No hook, no hash
  • OrphanedHash: Hash file exists, hook missing

PatchMode Behavior

Controls how rtk init modifies agent settings files:

Mode Flag Behavior
Ask (default) -- Prompts before settings changes, protected Pi/OMP overwrites, and definitively shared uninstalls; defaults to No if stdin not terminal
Auto --auto-patch Patches without prompting and approves protected Pi/OMP extension updates; for CI/scripted installs
Skip --no-patch Protected Pi/OMP actions leave files unchanged and exit nonzero; settings changes print manual instructions and succeed

Atomicity and Safety

All file operations use atomic writes (tempfile + rename) to prevent corruption on crash. Settings files are backed up to .bak before modification. All operations are idempotent -- running rtk init multiple times is safe.

Permission Model

RTK enforces a permission precedence that matches Claude Code's least-privilege default:

text
Deny > Defer > Ask > Allow (explicit) > Default (ask)

Rules are loaded from all Claude Code settings.json files (project + global, including .local variants). Only Bash(...) rules are extracted; other scopes (Read, Write) are ignored.

Quoted sh -c, bash -c, zsh -c and fish -c scripts add a
second command-parsing boundary. RTK checks deny rules against both the outer
wrapper and the parsed inner segments, including a wrapper behind a prefix the
rewrite strips (env bash -c '…', timeout 5 sh -c '…', a configured
transparent_prefixes entry — registry::shell_wrapper_positions). A shell
name that is merely an argument (grep -rn bash -c src) is not a wrapper. A
deny rule matching an inner segment yields Defer: RTK neither rewrites nor
decides, so the host sees the command exactly as typed.
A wrapper at the start of a segment is never auto-allowed: its strongest RTK
verdict is Ask. A wrapper behind a prefix is only checked for deny rules and
otherwise keeps the outer command's verdict, as on master. Ask-capable hosts
prompt, while other hosts retain their existing native permission flow.
Unsupported or ambiguous wrapper syntax is left unchanged for the host to
evaluate. Line continuations are joined before the check, as the rewrite joins
them.

Verdict Trigger rewrite_cmd exit Hook behavior
Deny permissions.deny rule matched 2 Passthrough — host tool handles denial
Ask permissions.ask rule matched 3 Rewrite + let host tool prompt user
Allow permissions.allow rule matched 0 Rewrite + auto-allow
Default No rule matched 3 Rewrite + let host tool prompt user
Defer permissions.deny rule matched inside a shell wrapper 1 Passthrough — command runs as without RTK

A deny or ask rule is matched against each segment as written, without the grammar a split leaves ({, !, (), and past each leading assignment or wrapper in turn (registry::matching_readings): env HUSKY=0 git push is read as HUSKY=0 git push and as git push, as Claude Code reads it. A reading need not be what the shell runs (nohup HUSKY=0 git push makes nohup look for a program named HUSKY=0), so it can only make a verdict stricter. Past 32 prefixes the command behind them is unread, and where a deny or ask rule exists the verdict is at least Ask. An allow rule matches the segment as written only, so a wrapper never inherits the allow of the command it runs.

A delegate that shells out to rtk rewrite and applies its own exec policy to
the result can set RTK_REWRITE_HOST=<agent> on that subprocess. For an agent
whose AgentPath records it as owning approval — OpenClaw is the only one —
Default renders as exit 0 instead of 3, because a Default verdict means no
rule matched and RTK asking as well would be a second gate sourced from Claude
Code's settings the runtime never opted into (#3908). An explicit Ask rule is
the user's own instruction, so it still renders as exit 3 and the host still
prompts. The verdict source is unchanged, and Deny still renders as exit 2 for
every delegate, so naming a host can never relax an explicit deny or discard an
explicit ask. An unknown or unset value keeps the table above — and a caller
that does not own approval should scrub the variable before invoking rtk rewrite, since it is inherited by every child process. See decision.rs's
ApprovalOwner.

Per-tool support

Tool ask support Behavior on Default
Claude Code (rtk-rewrite.sh) Yes permissionDecision: "ask" — user prompted
Copilot VS Code (rtk hook copilot) Yes permissionDecision: "ask" — user prompted
Cursor (rtk hook cursor) Ready permission: "ask", — users will be prompted when Cursor enforces the permission; in the meantime, allow
Gemini CLI (rtk hook gemini) No (allow/deny only) allow (limitation — no ask mode in Gemini)
Copilot CLI (rtk hook copilot) No updatedInput deny-with-suggestion (unchanged)
Codex (rtk hook codex) Native approval runs after rewrite Emit required protocol allow with updatedInput; Codex then evaluates the rewritten command normally
Trae (rtk hook trae) Host-owned approval Return only updatedInput; omit permissionDecision
Mistral Vibe (rtk hook vibe) No native ask surface passthrough — Vibe's own approval prompt fires on the rewritten command
OpenClaw (openclaw/index.ts → rtk rewrite) Host-owned approval (RTK_REWRITE_HOST=openclaw) Rewrite with no RTK prompt when no rule matched; an explicit Ask still exits 3 and the plugin prompts. OpenClaw's tools.exec.mode/security/ask decide. A Deny still exits 2 and the plugin blocks the call
Google Antigravity (rtk hook antigravity) Native approval runs after rewrite allow with overwrite.CommandLine — Antigravity evaluates permissions after hook

Implementation

  • permissions.rs — loads deny/ask/allow rules, evaluates precedence, returns PermissionVerdict
  • rewrite_cmd.rs — maps verdict to exit code (consumed by shell hook)
  • hook_cmd.rs — maps decisions to each agent's JSON protocol, including Codex updatedInput
  • discover/shell_wrapper.rs — shares conservative script-span parsing between rewrite and permission checks

Exit Code Contract

Hook processors in hook_cmd.rs must return Ok(()) on every path — success, no-match, parse error, and unexpected input. Returning Err propagates to main() and exits non-zero, which blocks the agent's command from executing. This violates the non-blocking guarantee documented in hooks/README.md.

Adding New Functionality

To add support for a new AI coding agent: (1) add the hook installation logic to src/hooks/init/ following the existing agent patterns, (2) if the agent requires a custom hook protocol (like Gemini's BeforeTool or Vibe's pre_tool), add a processor function in hook_cmd.rs and a matching HookCommands::<Agent> variant + AgentTarget::<Agent> enum entry in main.rs, (3) if the agent has installable permission surfaces (denylist / allowlist), wire them into permissions.rs::check_command_for via a new Host::<Agent> variant, and (4) update integrity.rs with the expected hash for the new hook file. Note that hook_check.rs::maybe_warn() only checks the Claude Code hook — other agents don't have an outdated-hook warning path. Test by running rtk init in a fresh environment and verifying the hook rewrites commands correctly in the target agent.


Overview (`src/learn/README.md`)

Learn — CLI Correction Detection

See also docs/contributing/TECHNICAL.md for the full architecture overview

Purpose

Analyzes Claude Code session history to detect recurring CLI mistakes — commands that fail then get corrected by the agent. Powers the rtk learn command, which identifies error patterns (unknown flags, wrong paths, missing args) and can auto-generate .claude/rules/cli-corrections.md to prevent them.

Key Types

  • ErrorType — UnknownFlag, CommandNotFound, WrongSyntax, WrongPath, MissingArg, PermissionDenied, Other(String)
  • CorrectionPair — Raw detection: wrong command + right command + error output + confidence score
  • CorrectionRule — Deduplicated pattern: wrong pattern + right pattern + occurrence count + base command

Dependencies

  • Uses: discover::provider::ClaudeProvider (session file discovery and command extraction), LazyLock/regex (error pattern matching), serde_json (JSON output)
  • Used by: src/main.rs (routes rtk learn command)

Detection Algorithm

  1. Extract all commands from JSONL sessions via ClaudeProvider
  2. Scan chronologically for fail-then-succeed pairs (same base command, first has error output, second succeeds)
  3. Classify the error type using regex patterns on the error output
  4. Assign confidence scores based on similarity and error clarity
  5. Deduplicate into rules (merge identical wrong->right patterns, count occurrences)
  6. Filter by --min-confidence and --min-occurrences thresholds

Overview (`src/parser/README.md`)

Parser Infrastructure

See also docs/contributing/TECHNICAL.md for the full architecture overview

Overview

The parser infrastructure provides a unified, three-tier parsing system for tool outputs with graceful degradation:

  • Tier 1 (Full): Complete JSON parsing with all structured data
  • Tier 2 (Degraded): Partial parsing with warnings (fallback regex)
  • Tier 3 (Passthrough): Raw output truncation with error markers

Architecture

text
┌─────────────────────────────────────────────────────────┐
│                    ToolCommand Builder                   │
│  Command::new("vitest").arg("--reporter=json")          │
└─────────────────────┬───────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────┐
│                   OutputParser<T> Trait                  │
│  parse() → ParseResult<T>                               │
│    ├─ Full(T)           - Tier 1: Complete JSON parse   │
│    ├─ Degraded(T, warn) - Tier 2: Partial with warnings │
│    └─ Passthrough(str)  - Tier 3: Truncated raw output  │
└─────────────────────┬───────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────┐
│                  Canonical Types                         │
│  TestResult, LintResult, DependencyState, BuildOutput   │
└─────────────────────┬───────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────┐
│                  TokenFormatter Trait                    │
│  format_compact() / format_verbose() / format_ultra()   │
└─────────────────────────────────────────────────────────┘

Usage Pattern

  1. Implement OutputParser for a tool — try JSON (Tier 1), fall back to regex (Tier 2), then passthrough (Tier 3)
  2. In command module: call Parser::parse(), then data.format(FormatMode::from_verbosity(verbose))
  3. Degradation warnings: print [RTK:DEGRADED] in verbose mode, [RTK:PASSTHROUGH] on full fallback

See src/parser/types.rs for the OutputParser trait and ParseResult enum.

Canonical Types

TestResult

For test runners (vitest, playwright, jest, etc.)

  • Fields: total, passed, failed, skipped, duration_ms, failures
  • Formatter: Shows summary + failure details (compact: top 5, verbose: all)

LintResult

For linters (eslint, biome, tsc, etc.)

  • Fields: total_files, files_with_issues, total_issues, errors, warnings, issues
  • Formatter: Groups by rule_id, shows top violations

DependencyState

For package managers (pnpm, npm, cargo, etc.)

  • Fields: total_packages, outdated_count, dependencies
  • Formatter: Shows upgrade paths (current → latest)

BuildOutput

For build tools (next, webpack, vite, cargo, etc.)

  • Fields: success, duration_ms, bundles, routes, warnings, errors
  • Formatter: Shows bundle sizes, route metrics

Format Modes

Compact (default, verbosity=0)

  • Summary only
  • Top 5-10 items
  • Token-optimized

Verbose (verbosity=1)

  • Full details
  • All items (up to 20)
  • Human-readable

Ultra (verbosity=2+)

  • Symbols: ✓✗⚠ pkg: ^
  • Ultra-compressed
  • 30-50% token reduction

Error Handling

ParseError Types

  • JsonError: Line/column context for debugging
  • PatternMismatch: Regex pattern failed
  • PartialParse: Some fields missing
  • InvalidFormat: Unexpected structure
  • MissingField: Required field absent
  • VersionMismatch: Tool version incompatible
  • EmptyOutput: No data to parse

Degradation Warnings

text
[RTK:DEGRADED] vitest parser: JSON parse failed at line 42, using regex fallback
[RTK:PASSTHROUGH] playwright parser: Pattern mismatch, showing truncated output

Migration Guide

Existing Module → Parser Trait

Replace direct filter_*_output() calls with Parser::parse() + FormatMode. Key change: add --reporter=json flag injection, match on ParseResult (Full/Degraded/Passthrough), format with data.format(mode). Degraded and Passthrough tiers handle tool version changes gracefully.

Testing

Run cargo test parser::tests. Each parser should have tier validation tests: assert result.tier() == 1 for valid JSON fixtures, tier() == 2 for regex fallback inputs, and tier() == 3 for completely malformed output.

Benefits

  1. Maintenance: Tool version changes break gracefully (Tier 2/3 fallback)
  2. Reliability: Never silent failures or false data
  3. Observability: Clear degradation markers in verbose mode
  4. Token Efficiency: Structured data enables better compression
  5. Consistency: Unified interface across all tool types
  6. Testing: Fixture-based regression tests for multiple versions

Roadmap

Phase 4: Module Migration

  • vitest_cmd.rs → VitestParser
  • playwright_cmd.rs → PlaywrightParser
  • pnpm_cmd.rs → PnpmParser (list, outdated)
  • lint_cmd.rs → EslintParser
  • tsc_cmd.rs → TscParser
  • gh_cmd.rs → GhParser

Phase 5: Observability

  • Extend tracking.db: parse_tier, format_mode
  • rtk parse-health command
  • Alert if degradation > 10%

Overview (`src/cmds/cloud/README.md`)

Cloud and Infrastructure

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • aws_cmd.rs — 25 specialized filters covering STS, S3, EC2, ECS, RDS, CloudFormation, CloudWatch Logs, Lambda, IAM, DynamoDB, EKS, SQS, Secrets Manager. Forces --output json for structured parsing, uses force_tee_hint() for truncation recovery, strips Lambda secrets. Shared runner run_aws_filtered() handles boilerplate for JSON-based filters; text-based filters (S3 ls, S3 sync/cp) have dedicated runners
  • container.rs handles Docker, Kubernetes, and OpenShift; DockerCommands, KubectlCommands, and OcCommands sub-enums in main.rs route to container::run() -- uses passthrough for unknown subcommands
  • curl_cmd.rs truncates long responses, saves full output to file for recovery
  • wget_cmd.rs wraps wget with output filtering
  • psql_cmd.rs filters PostgreSQL query output

Overview (`src/cmds/dotnet/README.md`)

.NET Ecosystem

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • dotnet_cmd.rs uses DotnetCommands sub-enum in main.rs
  • Internal helper modules (dotnet_trx.rs, dotnet_format_report.rs, binlog.rs) are only used by dotnet_cmd.rs -- they parse specialized .NET output formats (TRX XML, binary logs, format reports)
  • Test fixtures are in tests/fixtures/dotnet/ (JSON and text formats)

Overview (`src/cmds/git/README.md`)

Git and VCS

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • git_cmd.rs uses trailing_var_arg = true + allow_hyphen_values = true so native git flags (--oneline, --cached, etc.) pass through correctly
  • Flag grammar (which flags take a value, what -u/-p mean) is per-subcommand, not shared wholesale between log/diff/show/stash show — see src/core/README.md
  • Default git status uses --porcelain -b so the compact output never exceeds raw git status (an untracked directory collapses to a single line, matching git's default); branch/short-only flags reuse the compact path, other explicit args still pass through unchanged
  • Global git options (-C, --git-dir, --work-tree, --no-pager) are prepended before the subcommand
  • Exit code propagation is critical for CI/CD pipelines
  • glab_cmd.rs declares -R/--repo and -g/--group at the clap level; they are appended to the glab args (not prepended) so subcommand dispatch stays intact
  • has_output_flag() short-circuits to passthrough when the user explicitly requests -F / --output / --json (avoids double JSON injection)
  • should_passthrough_view() redirects mr/issue view to passthrough when --web or --comments is set
  • JSON handlers use the local run_glab_json<F>() helper wrapping runner::run_filtered + RunOptions::stdout_only().early_exit_on_failure().no_trailing_newline(); on JSON parse error, falls back to the raw stdout (glab sometimes emits plain text for empty results)
  • ci status uses text-keyword parsing (glab doesn't support -F json for this subcommand); when no English status keyword is recognized (non-English locale), returns raw verbatim
  • ci trace uses ANSI-stripping + GitLab section-marker filtering + runner/git/artifact boilerplate removal; kept as text-only filter, not JSON
  • release list falls back to raw output when the glab 1.82+ format doesn't match the legacy tab-delimited parser
  • Pipeline / merge-status indicators use text tags ([ok], [fail], [cancel], [run], [pend], [skip], [conflict]) to match gh_cmd.rs and avoid multi-byte rendering quirks

Cross-command

  • gh_cmd.rs imports compact_diff() from git_cmd.rs for diff formatting; markdown helpers (filter_markdown_body, filter_markdown_segment) are defined in gh_cmd.rs itself
  • glab_cmd.rs also uses compact_diff() from git_cmd.rs for mr diff; its filter_markdown_body is currently duplicated from gh_cmd.rs (shared-module refactor deferred)
  • diff_cmd.rs is a standalone ultra-condensed diff (separate from git diff)

glab vs gh JSON schema quick-ref

Aspect gh glab
Notation #42 !42
States OPEN/MERGED/CLOSED opened/merged/closed
Author author.login author.username
URL field url web_url
Body field body description
Merge check mergeable merge_status (can_be_merged / cannot_be_merged)
CI status statusCheckRollup head_pipeline.status
Labels labels (array of objects) labels (array of strings)
Reviewers reviewRequests/reviews reviewers (array of objects with username)

Overview (`src/cmds/go/README.md`)

Go Ecosystem

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • go_cmd.rs uses GoCommands sub-enum in main.rs (same pattern as git/cargo)
  • go test outputs NDJSON (-json flag injected by RTK) -- parsed line-by-line as streaming events
  • golangci_cmd.rs forces --out-format=json for structured parsing

Overview (`src/cmds/js/README.md`)

JavaScript / TypeScript / Node

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • utils::package_manager_exec() auto-detects pnpm/yarn/npm -- JS modules should use this instead of hardcoding a package manager
  • lint_cmd.rs is a cross-ecosystem router: detects Python projects and delegates to mypy_cmd or ruff_cmd
  • vitest_cmd.rs uses the parser/ module for structured output parsing
  • playwright_cmd.rs uses the parser/ module for test result extraction

Cross-command

  • lint_cmd routes to cmds/python/mypy_cmd and cmds/python/ruff_cmd for Python projects
  • prettier_cmd is also called by cmds/system/format_cmd as a format dispatcher target

Overview (`src/cmds/jvm/README.md`)

JVM ecosystem filters

Filters for JVM-based build tools.

Module Tool(s) Modes
gradlew_cmd.rs ./gradlew, gradlew.bat, gradle Build / Test / ConnectedTest / Lint / Dependencies — streaming line filter + passthrough
mvn_cmd.rs mvn, ./mvnw, mvnw.cmd, mvnd Test / Compile / Package / Passthrough — buffered single-pass filter per phase

Maven (`mvn_cmd.rs`)

Phase routing (detect_phase):

Phase Goals Filter
Test test, integration-test (Failsafe = Surefire shape) filter_surefire
Compile compile, test-compile filter_compile
Package package, install, verify, deploy filter_package
Passthrough clean, site, dependency:*, --version, --help, empty, any unrecognised goal none

Key behaviours:

  • ANSI strip first in every filter — real Maven output contains colour escapes.

  • English-footer guard — if neither BUILD SUCCESS nor BUILD FAILURE appears as a trimmed line suffix, return the ANSI-stripped raw input unchanged. Protects non-English locales.

  • Verbose bypass — -X, --debug, -e, --errors skip filtering (run_passthrough). User asked for detail; respect it.

  • Surefire block collapse — Surefire emits [INFO] Running <FQN> … [INFO] Tests run: N, Failures: F, Errors: E, …, Time elapsed: T s - in <FQN>. The filter buffers each block and emits it only when F > 0 or E > 0. Passing blocks (the bulk of healthy-project output) are dropped silently. Failing blocks are emitted with framework stack frames stripped via a deny-list (at org.junit., at java.util., at sun.reflect., etc.).

  • Multi-failure classes (trail re-arm) — when a single class has several failing tests, Surefire 3.x emits one blank-separated detail block per failing test under a single close line. When a failure trail ends at a blank line, the state machine arms a re-entry: the next per-test subline ([ERROR] FQN.method -- Time elapsed: … <<< FAILURE! or <<< ERROR!) re-enters the trail with the same keep/drop decision, so every failure message survives (and a capped class drops all its blocks). Any other non-blank line disarms the re-entry.

  • <<< ERROR! markers — per-test sublines use <<< ERROR! for thrown (non-assertion) exceptions; the close-line regex also tolerates an ERROR! marker defensively (Surefire 3.5.5 emits FAILURE! even for errors-only classes — failure detection keys off the Failures/Errors counts, not the marker).

  • Help-boilerplate stripping (all modes) — the post-failure block Maven emits after [ERROR] Failed to execute goal (See …, -> [Help 1], Re-run Maven, To see the full stack trace, For more information, help URLs, bare [ERROR] dividers) is dropped in quiet and non-quiet filters alike (shared BOILER_PREFIXES). Deliberately kept as signal: Failed to execute goal itself and the multi-module resume hint ([ERROR] After correcting the problems… + [ERROR] mvn <args> -rf :module — tells the user/agent how to resume the build). Real durations (Time elapsed: … s, Total time: …) ship untouched — the numbers are diagnostic signal.

  • Wrapper detection — ./mvnw (POSIX) and mvnw.cmd (Windows) detected via string-literal Command::new (semgrep-safe); falls back to resolved_command("mvn").

  • Maven Daemon (rtk mvnd) — mvnd is a separate entry point (run_daemon), not a mvn alias: it shares phase detection and every filter, but always executes mvnd and is never substituted by a ./mvnw wrapper found in the working directory. The daemon's rolling console UI only engages on a TTY, so what rtk captures is ordinary Maven log output and the existing filters apply unchanged. Two daemon-specific quirks are handled explicitly:

    • [INFO]-prefixed blanks — the daemon logger prefixes even empty lines with [INFO] , so the Surefire failure trail's blank-line terminator uses is_blank_separator (a bare [INFO] counts as blank). Without it the trail never closes and everything after the last failing class passes through verbatim.
    • Parallel-reactor lanes — mvnd builds multi-module reactors in parallel by default and prefixes per-module lines with [module] , interleaved line-by-line. split_lane classifies on the prefix-stripped view while emitting the original (module identity survives), and each module gets its own SurefireLane (block machine + compile-continuation flag + summary-open flag) so a passing close from one module can't be attributed to another's open block. Three consequences of the interleaving are handled explicitly: unprefixed raw lines (stack traces, javac symbol: / location: continuations) go to the lane in an active failure trail before any lane merely holding an open block (raw_owner) — otherwise a module opening a block between a failing close and its exception swallows the diagnostics and drops them when it closes green — and when ownership is genuinely ambiguous (several plain blocks open, no trail) the line is preserved verbatim rather than routed to a guess. The [ERROR] Failures: budget stays reactor-wide (FailuresSummaryCap, reset only when a header opens the first summary), so interleaved per-module summaries share one cap instead of each getting MAX_MVN_FAILING_CLASSES.

    One known limit remains, not a regression — it already applies to mvn -T: daemon status chatter (Connecting to the daemon, …) carries no [INFO]/[ERROR] prefix, so the keep-list filters (filter_surefire, filter_compile, filter_package) drop it — but filter_quiet has no keep-list, only a deliberate "keep anything else" safety net, so rtk mvnd -q passes that chatter through where rtk mvn -q emits nothing.

  • Reactor Summary preservation — for multi-module builds, the trailing Reactor Summary for <root> block with per-module SUCCESS/FAILURE rows is kept (toggled by a [INFO] Reactor Summary for header and cleared on BUILD SUCCESS / BUILD FAILURE).

  • Failure cap — both the count of emitted failing test classes and the size of the [ERROR] Failures: summary block are bounded by MAX_MVN_FAILING_CLASSES = CAP_WARNINGS (the shared test-failure cap class from src/core/truncate.rs, same binding as pytest/rspec/rake/runner). Excess emissions are replaced by a single … +N more failing test classes / … +N more failures tail (canonical join_with_overflow shape) to keep large failure sets compact; the raw output stays recoverable via the tee [full output: …] hint. Per the core cap policy, a cap of 0 means summary-only: no blocks emitted, the tail still counts every dropped class.

Token-savings tests run inline as part of cargo test --all. The mvn fixtures (gzipped, ~1100 lines each; flate2, already in Cargo.toml, decompresses the ~3 KB gzipped fixtures in milliseconds) verify ≥90% savings for mvn test and ≥85% for mvn install. The mvnd fixtures are smaller synthetic captures and are held to the repo-wide ≥60% savings floor instead (mvnd_reactor_pass_savings, mvnd_test_fail_savings, mvnd_parallel_reactor_fail_savings, mvnd_compile_error_savings; actuals run 64.9–76.5%).

Integrity-check whitelist

Commands::Aws, Commands::Psql, Commands::Wc, Commands::Mypy, Commands::Gradlew, Commands::Mvn, and Commands::Mvnd are included in is_operational_command in src/lib.rs because hook rewrites invoke them as RTK subcommands. The whitelist is intentionally opt-in: a newly added rewritten command must be listed or it will not receive the runtime SHA-256 hook-integrity check.

Gradle (`gradlew_cmd.rs`)

See module docs and the gradle PR (feat/gradlew-android-support) for rationale. Streaming filter chosen because Gradle output is task-line-based, not block-based — unlike Maven Surefire.


Overview (`src/cmds/php/README.md`)

PHP

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • php_cmd.rs summarizes php -l syntax checks and routes php artisan* to specialized helpers
  • artisan_cmd.rs cleans Artisan output and applies runner-aware filtering for php artisan test
  • phpunit_cmd.rs strips progress/header noise and keeps failure details + final summary
  • phpstan_cmd.rs injects JSON output by default and emits compact file/line error summaries
  • ecs_cmd.rs condenses EasyCodingStandard output while preserving file paths and error lines
  • pest_cmd.rs runs Pest with compact progress suppression and test-focused output
  • paratest_cmd.rs runs ParaTest with compact progress suppression and test-focused output
  • test_output.rs provides shared PHPUnit/Pest/ParaTest output filtering logic
  • utils.rs resolves local vendor/bin/* tools first, then falls back to global binaries

Overview (`src/cmds/python/README.md`)

Python Ecosystem

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • pytest_cmd.rs uses a state machine text parser (no JSON available from pytest)
  • ruff_cmd.rs uses JSON for check mode (--output-format=json) and text filtering for format mode
  • sqlfluff_cmd.rs injects --format json for lint mode and groups violations by rule and file; other subcommands pass through capped
  • Ruff and SQLFluff list identical descriptions once beside a visible top rule.
    Locations retain the rule code; differing messages and rules outside the
    summary cap keep their descriptions beside each location.
  • pip_cmd.rs auto-detects uv as a pip alternative and routes accordingly
  • uv_cmd.rs preserves uv run environment semantics: the program's own output passes through on success (bounded, with a tee hint), diagnostics are extracted on failure
  • python -m pytest and python3 -m mypy are rewritten by the hook registry to rtk pytest / rtk mypy

Cross-command

  • ruff_cmd is called by cmds/js/lint_cmd and cmds/system/format_cmd for Python projects
  • mypy_cmd is called by cmds/js/lint_cmd when detecting Python type checking

Overview (`src/cmds/ruby/README.md`)

Ruby on Rails

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • rake_cmd.rs filters Minitest output via rake test / rails test; state machine text parser, failures only (85-90% reduction)
  • rspec_cmd.rs uses JSON injection (--format json) with text fallback; failures only (60%+ reduction)
  • rubocop_cmd.rs uses JSON injection, groups by cop/severity (60%+ reduction)
  • All three modules use ruby_exec() from utils.rs to auto-detect bundle exec when a Gemfile exists
  • TOML filter bundle-install.toml strips Using lines from bundle install/update (90%+ reduction)

Overview (`src/cmds/rust/README.md`)

Rust Ecosystem

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • cargo_cmd.rs uses restore_double_dash() fix: Clap strips -- but cargo needs it for test flags (e.g., cargo test -- --nocapture)
  • runner.rs is a generic two-mode runner (err = stderr only, test = failures only) used as fallback for commands without a dedicated filter. It executes argv directly by default; --shell <name> '<script>' is the explicit escape hatch for shell syntax.
  • runner.rs is also referenced by other modules outside this directory as a generic command executor

Overview (`src/cmds/system/README.md`)

System and Generic Utilities

Part of src/cmds/ — see also docs/contributing/TECHNICAL.md

Specifics

  • read.rs uses core/filter for language-aware code stripping (FilterLevel: none/minimal/aggressive)
  • search.rs backs both rtk grep and rtk rg: it runs the invoked engine (never substituting one for the other) and groups its output, reading core/config for limits.grep_max_results and limits.grep_max_per_file. Format-altering flags (-c, -l, -L, -o, -Z) bypass RTK filtering and run raw, except a bare file list (-l, -L, --files with no other shape flag), whose shared directory prefix is folded into a (N files) header under the never_worse guard.
  • ctest_cmd.rs takes the run total from the first result line (covering --stop-on-failure, disabled tests, and forwarded suites) and validates both result lines and the summary against it, deduplicates retries by test number+name, folds wrapped result lines until their terminator, falls back to the raw FAILED: trailer for unparsed failures, keeps the error trailer behind an empty run, labels and caps failure details with tee recovery, and attributes diagnostics safely under -j; explicit verbose/show-only/help/version flags and dashboard modes bypass filtering, except -T Test, which prints ordinary test output and stays filtered.
  • local_llm.rs (rtk smart) uses core/filter for heuristic file summarization
  • format_cmd.rs is a cross-ecosystem dispatcher: auto-detects and routes to prettier_cmd or ruff_cmd (black is handled inline, not as a separate module)
  • summary.rs executes argv directly before summarizing output. Shell-specific syntax requires --shell <name> '<script>'; it never guesses from $SHELL.

Cross-command

  • format_cmd routes to cmds/js/prettier_cmd and cmds/python/ruff_cmd

Guide Index (`docs/guide/index.md`)


title: RTK Documentation sidebar: order: 1

RTK — Rust Token Killer

RTK is a CLI proxy that sits between your AI assistant and your development tools. It filters command output before it reaches the LLM, keeping only what matters and discarding boilerplate, progress bars, and noise.

Result: fewer bash output bytes reaching the LLM per command, without changing how you work. You run git status as usual — RTK's hook intercepts it, filters the output, and the LLM sees a compact 3-line summary instead of 40 lines.

How it works

text
Your AI assistant runs:  git status
                              ↓
              Hook intercepts (PreToolUse)
                              ↓
              rtk git status  (transparent rewrite)
                              ↓
     Raw output: 40 lines     →     Filtered: 3 lines
                              ↓
              LLM sees the compact output

Zero config changes to your workflow. The hook handles everything automatically.

What the savings mean

RTK reduces bash output bytes — the output a shell command sends back before your agent reads it. That is not the same as reducing your bill by the same amount.

Bash output is one contributor to input tokens, alongside your prompt, the system prompt and conversation history. Input tokens are in turn only part of the bill, which also counts output tokens. The reduction dilutes at every step.

See How RTK Savings Work for the full picture, including why the token counts RTK reports are estimates.

What RTK optimizes

Dozens of commands across all major ecosystems — Git, Cargo/Rust, JavaScript, Python, Go, Ruby, .NET, Docker/Kubernetes, and more. See What RTK Optimizes for the full list.

Get started

  1. Installation — Install RTK and verify you have the right package
  2. Quick Start — Connect to your AI assistant in 5 minutes
  3. Supported Agents — Claude Code, Cursor, Copilot, Gemini, and more

Measure your savings

bash
rtk gain           # total savings across all sessions
rtk gain --daily   # day-by-day breakdown
rtk gain --weekly  # weekly aggregation

See Token Savings Analytics for export formats and analysis workflows.

Analyze your usage

bash
rtk discover       # find commands that ran without RTK (missed savings)
rtk session        # RTK adoption rate per Claude Code session

See Discover and Session for details.

Go further

Two optional layers sit on top of the hooks. Neither changes anything until you opt in.

Further reading


Guide Pipelines Overview (`docs/guide/pipelines/overview.md`)


title: Pipelines sidebar: order: 1

The RTK pipeline

RTK saves tokens by intercepting what your coding agent reads and sends. In v1.0 what it does is a pipeline: a file that says when RTK acts, on what, and what it does. Every built-in behaviour is written in that same grammar, so you can read it (rtk pipeline show rtk:default), change one line of it, or replace it.

The file is agent-agnostic. You write shell, read, before_tool; RTK translates to Claude Code, Cursor, Gemini CLI or whatever you set up with rtk init. A pipeline written on one machine works on another.

:::note[Full edition only]
The pipeline engine is absent from the Apache-2.0 edition. See Editions and Licensing.
:::

What it provides

RTK can act in two places, and a pipeline can add two more kinds of things.

Where What RTK can do there Needs
hook — inside your agent, around each tool call before a tool runs: rewrite a shell command through rtk, refuse a call, or hand the work to a cheaper model. After a tool runs: compress its output before the agent reads it. rtk init (installs the hooks)
proxy — between your agent and the model provider compress blocks already in the conversation and place the cache breakpoint the RTK Proxy running
directives text RTK puts in front of the agent: how to behave, what commands to use nothing
tasks commands the agent can run that delegate work to a cheaper model — rtk bulk-read, rtk code-write a model key you provide

Three files you might write

Nothing. Upgrade, run rtk init, and you have rtk:default — RTK exactly as it worked before: the shell-command rewrite, the awareness file, your filters.toml, your config.toml. No new hooks are registered, nothing changes.

Turn on more. Output compression after every tool call, and the proxy's cache layer, are reference pipelines you opt into — rtk init offers the proxy one; rtk:posthook you add by hand, as below.

toml
# ~/.config/rtk/pipeline.toml
[pipeline]
extends = ["rtk:posthook", "rtk:proxy"]

Tune one thing.

toml
[pipeline]
extends = "rtk:delegate-reads"

[override.shunt-large-read]
when.file_lines_gt = 500

This raises the threshold of the inherited shunt-large-read rule from 350 lines; the rest of its when is kept.

Adopt a behaviour and supply what only you can.

toml
[pipeline]
extends = "rtk:delegate-reads"

[upstream.flash]
provider = "google"
model = "gemini-2.5-flash"
api_key_env = "GEMINI_API_KEY"

[task.bulk-read]
upstream = "flash"
[task.code-write]
upstream = "flash"

Where to go next

Page What it covers
Writing a Pipeline where files live, the rule tree, actions, routing, overrides
Conditions and Judges every when key, the call context, and AI classifiers (judges)
Methods the transforms rules chain together, your own chains and filters
Directives text RTK puts in front of the agent
Tasks and Upstreams delegating work to a cheaper model
Reference Pipelines the six built-ins, as literal files
Pipeline Commands show, validate, explain, shadow, share, adopt
Errors and Warnings every diagnostic code, and the engine's limits
Plugins new methods and task kinds in Rust

Where things land on disk

What Where
user pipeline ~/.config/rtk/pipeline.toml, ~/.config/rtk/pipelines/*.toml
project pipeline (trust-gated) .rtk/pipeline.toml, .rtk/pipelines/*.toml
load problems (hooks never print) ~/.local/share/rtk/pipeline-status.json → rtk pipeline status, rtk gain
opt-in decision log ~/.local/share/rtk/pipeline.log → rtk pipeline log tail

Paths use the Linux spelling; on macOS the user directory is ~/Library/Application Support/rtk/, on Windows %APPDATA%\rtk\.

Hooks are registered for the events and tools your pipeline uses at the time of rtk init. After adding a rule for a new tool or event, run rtk init -g again — rtk pipeline validate prints hook needed: … until you do.

Version skew: what degrades, what refuses

Pipelines travel — between machines, rtk versions, and (via rtk pipeline share) between people. The validator's contract is split by what a rule can do:

  • Transform rules fail open. A compression rule naming a method or condition this rtk does not know gets a warning (W014/W015), that rule is skipped, and the rest of the file stays active. A newer pipeline on an older rtk loses an optimization, never the whole file.
  • Control rules fail closed. A rule with an action (deny, route, rewrite, passthrough) must validate completely — an unknown condition or task kind is a hard error for that file. A protection can never look installed while being silently inert.

Upgrading from RTK ≤ 0.51

Nothing to do. rtk:default is the RTK you had; there is no migration command.

You had In v1.0
filters.toml (user or project) unchanged — still the home of custom command filters, read by the rewrite as before
[hooks] exclude_commands, transparent_prefixes unchanged, in config.toml
[awareness] level unchanged; equivalent to [override.rtk-awareness] level = … if you prefer it in the pipeline
PreToolUse hook unchanged; PostToolUse is registered only if you enable rtk:posthook

Guide Proxy Overview (`docs/guide/proxy/overview.md`)


title: RTK Proxy sidebar: order: 1

RTK Proxy

rtk gain and the agent hooks work on bash output — what a command prints before your agent reads it. The RTK Proxy works one layer up, on the request your agent sends to the model: conversation history, tool results, file contents. It is a local HTTP daemon your agent talks to instead of talking to the model provider directly. The daemon binary is rtk-proxy; you drive it with rtk llm-proxy.

text
Claude Code ──▶ rtk-proxy ──▶ api.anthropic.com
                     │
                     ├─ normalize: strip ANSI / redraws / dupes
                     ├─ your proxy.request.tool_result rules
                     ├─ (opt-in) cache-align
                     └─ record usage ──▶ rtk gain

It is local: one background process on your machine. It does four things — intercept, compress, forward, record — and does not store prompt or completion text (the opt-in CCR keeps the originals it compresses). What it compresses is in What the Proxy Compresses; its settings and endpoints are in Proxy Configuration.

Both layers are optional and independent. You can run the hooks without the proxy, the proxy without the hooks, or both — rtk gain reports them side by side.

:::note[Separate download, separate licence]
The daemon is a second binary, rtk-proxy, shipped as its own release archive (rtk-proxy-<target>). It is not inside the rtk archive, because it is licensed separately — the rtk CLI is Apache-2.0, rtk-proxy is BUSL-1.1. See Editions and licensing.
:::

Install the daemon

Download the rtk-proxy-<target> archive for your platform from the releases page and put the binary next to rtk (anywhere on your PATH also works).

From a source checkout:

bash
cargo build --release --manifest-path proxy/Cargo.toml

rtk looks for the daemon next to its own binary first, then on PATH. If it cannot find it, rtk llm-proxy tells you exactly where it looked.

Quick start

bash
rtk llm-proxy setup     # check the daemon is present, list agents it can serve
rtk llm-proxy start     # launch it in the background (no-op if already up)
rtk llm-proxy status    # is it running and reachable?
rtk llm-proxy stats     # usage and savings from the running daemon
rtk llm-proxy stop      # ask it to shut down

setup is the one to run first: it reports which AI agents on this machine can be pointed at the proxy, and how.

You rarely need start by hand — rtk claude starts the daemon for you. See Getting your agent onto the proxy.

Getting your agent onto the proxy

Two independent ways in. rtk init configures your agent once and persists it; rtk <harness> needs no configuration at all.

`rtk init -g` — the guided setup

On a Claude Code init, RTK asks:

text
Set up the RTK proxy (routes Claude Code's API traffic through a local daemon)? [y/N]

Say yes and it writes env.ANTHROPIC_BASE_URL into settings.json and adds extends = "rtk:proxy" to your pipeline. Then it asks how the daemon should come back after a reboot:

Choice What it does
1. SessionStart hook (default) Registers a hook that starts the daemon when a Claude session begins
2. Background service rtk llm-proxy service install — a per-user service, independent of any agent
3. Neither Prints the export line and leaves settings.json untouched

Declining leaves everything alone: "Skipped — enable later with rtk init -g or rtk llm-proxy start --env."

If you already have the base URL set but nothing restarting the daemon, re-running rtk init -g asks only the persistence question.

The question is only asked when the daemon can actually run: the rtk-proxy binary is installed (next to rtk or on PATH) or an rtk-proxy already answers on 127.0.0.1:8080. Otherwise init says the proxy was not offered and leaves settings.json alone. rtk init -g --uninstall removes the env.ANTHROPIC_BASE_URL this flow wrote, when it still points at the RTK daemon; a base URL aimed anywhere else is kept.

What the SessionStart hook actually does

It does not do the redirection — that is the settings.json entry above. The hook is a keep-alive: when a session whose ANTHROPIC_BASE_URL points at a loopback address finds nothing listening there, it runs rtk llm-proxy start.

It reports through systemMessage, which is user-visible and never enters model context — RTK spends no tokens configuring itself. Every failure path stays silent rather than disrupting the session.

This route only exists on harnesses that have a SessionStart hook, which is why the next one matters.

`rtk ` — the launcher

bash
rtk claude

One command, no configuration, nothing persisted: it starts the daemon if it is not already up, points the harness at it, and execs the real binary. On Unix this is a genuine exec, so signals, stdio and the exit code behave exactly as if you had launched the harness directly.

This route needs no hook system. Not every harness has a SessionStart hook, but most offer a base-URL override — so this is how an agent without hooks still gets onto the proxy.

The daemon is agent-agnostic by design: it speaks the provider's dialect, not the agent's. A subcommand exists only where that redirection has been verified end to end, so rtk <harness> resolving is itself the signal that traffic really goes through the daemon. Today the list is:

Subcommand Base-URL variable
rtk claude ANTHROPIC_BASE_URL

A harness absent from the list is never silently unproxied — it falls through to plain passthrough. rtk llm-proxy setup reports what can be launched on this machine.

The launcher never blocks you: if the daemon cannot start, the harness launches unproxied with a warning rather than failing.

`--env` — one shell, by hand

bash
eval "$(rtk llm-proxy start --env)"

Any agent launched from that shell routes through the proxy. This is choice 3 above, done manually.

Binding to another address

Start it on a different address with --bind:

bash
rtk llm-proxy start --bind 127.0.0.1:9090

Every subcommand that talks to a running daemon (stop, status, stats) takes the same --bind flag, so you can manage a proxy that is not on the default 127.0.0.1:8080.

Runtime settings and background service

Two subcommands forward straight through to the daemon's own CLI:

bash
rtk llm-proxy config <args...>    # get/set runtime settings on a running proxy
rtk llm-proxy service <args...>   # manage a per-user background service

Run either with --help to see the daemon's own options; the settings themselves are listed in Proxy Configuration. service is choice 2 of the guided setup — a per-user background service that survives logins independently of any agent.

Where it keeps its state

One fixed location, so the daemon's state does not depend on the shell's working directory when it was started:

Path Contents
~/.config/rtk/llm-proxy/ daemon log and databases — on macOS ~/Library/Application Support/rtk/llm-proxy/, on Windows %APPDATA%\rtk\llm-proxy\
rtk-proxy.log daemon log, started fresh on every start
rtk-proxy.db usage records — the file rtk gain reads
rtk-proxy-memory.db originals kept by CCR (opt-in), 7 days

Because the usage database lives at a known path, rtk gain can report proxy savings whether or not the daemon is currently running.

Seeing the savings

With the proxy running, a bare rtk gain switches to a live dashboard covering both layers. See Live dashboard and Attribution.

bash
rtk gain              # live dashboard (proxy running, interactive terminal)
rtk gain --text       # force the static view
rtk gain --text --by-method  # which compression method saved what

TELEMETRY (`docs/TELEMETRY.md`)

Telemetry

RTK collects anonymous, aggregate usage metrics once per day to help improve the product. Telemetry is disabled by default and requires explicit consent during rtk init or rtk telemetry enable.

Data Collector

Entity: RTK AI Labs
Contact: [email protected]

Why we collect telemetry

RTK supports 100+ commands across 15+ ecosystems. Without telemetry, we have no way to know:

  • Which commands are used most and need the best filters
  • Which filters are underperforming and need improvement
  • Which ecosystems to prioritize for new filter development
  • How much value RTK delivers to users (token savings in $ terms)
  • Whether users stay engaged over time or churn after trying RTK

This data directly drives our roadmap. For example, if telemetry shows that 40% of users run Python commands but only 10% of our filters cover Python, we know where to invest next.

How it works

  1. Once per day (23-hour interval), RTK sends a single HTTPS POST to our telemetry endpoint
  2. The ping runs in a background thread and never blocks the CLI (2-second timeout)
  3. A marker file prevents duplicate pings within the interval
  4. If the server is unreachable, the ping is silently dropped — no retries, no queue

Source code: src/core/telemetry.rs

What is collected

Identity (anonymous)

Field Example Purpose
device_hash a3f8c9... (64 hex chars) Count unique installations. SHA-256 of a per-device random salt stored locally (~/.local/share/rtk/.device_salt). Not reversible. No hostname or username included.

Environment

Field Example Purpose
version 0.34.1 Track adoption of new versions
os macos Know which platforms to support and test
arch aarch64 Prioritize ARM vs x86 builds
install_method homebrew Understand distribution channels (homebrew/cargo/script/nix)

Usage volume

Field Example Purpose
commands_24h 142 Daily activity level
commands_total 32888 Lifetime usage — segment light vs heavy users
top_commands ["git", "cargo", "ls"] Most popular tools (names only, max 5)
tokens_saved_24h 450000 Daily value delivered
tokens_saved_total 96500000 Lifetime value delivered
savings_pct 72.5 Overall effectiveness

Quality (filter improvement)

Field Example Purpose
passthrough_top ["git:15", "npm:8"] Top 5 commands with 0% savings — these need filters
parse_failures_24h 3 Filter fragility — high count means filters are breaking
low_savings_commands ["rtk docker ps:25%"] Commands with weighted savings rate <30%, net-negative ones included — filters to improve. Rate is SUM(saved)/SUM(input) over every call of the command, the same figure as the rtk gain By Command table, so high-volume calls are not diluted by passthrough calls. Exact 0% is left to passthrough_top.
avg_savings_per_command 68.5 Unweighted average across distinct command names (each filter counts once regardless of invocation volume). Each command's individual rate is weighted by volume before the outer average; commands that never had any input are skipped.
recall_mode sqlite Which recovery mode is active (sqlite/tee/disabled)
recall_stats [{"filter":"grep","mode":"sqlite","elisions":142,"recalls":9}] Per-filter counters: how often elided output is retrieved — calibrates filter caps. Filter names come from a fixed allowlist of rtk filter families; anything else is folded into other. No hashes, no paths, no command arguments, no output content.

Ecosystem distribution

Field Example Purpose
ecosystem_mix {"git": 45, "cargo": 20, "js": 15} Category percentages — where to invest filter development

Retention (engagement)

Field Example Purpose
first_seen_days 45 Installation age in days
active_days_30d 22 Days with at least 1 command in last 30 days — measures stickiness

Economics

Field Example Purpose
tokens_saved_30d 12000000 30-day token savings for trend analysis
estimated_savings_usd_30d — A USD value derived from the estimated tokens saved and a fixed internal constant. It is not a measured cost and does not reflect any provider's pricing

Adoption

Field Example Purpose
hook_type claude Which AI agent hook is installed (claude/gemini/codex/cursor/none)
custom_toml_filters 3 Number of user-created TOML filter files — DSL adoption

Configuration (user maturity)

Field Example Purpose
has_config_toml true Whether user has customized RTK config
exclude_commands_count 2 Commands excluded from rewriting — high count may indicate frustration
projects_count 5 Distinct project paths — multi-project = power user

Feature adoption

Field Example Purpose
meta_usage {"gain": 5, "discover": 2} Which RTK features are actually used

What is NOT collected

  • Source code or file contents
  • Full command lines or arguments (only tool names like "git", "cargo")
  • File paths or directory structures
  • Secrets, API keys, or environment variable values
  • Repository names or URLs
  • Personally identifiable information
  • IP addresses (not stored in telemetry pings; stored temporarily in erasure audit log for accountability, anonymized after 6 months)

Telemetry requires explicit opt-in consent (GDPR Art. 6, 7). Consent is requested during rtk init or via rtk telemetry enable. Without consent, no data is sent.

bash
rtk telemetry status     # Check current consent state
rtk telemetry enable     # Give consent (interactive prompt)
rtk telemetry disable    # Withdraw consent
rtk telemetry forget     # Withdraw consent + delete local data + request server erasure

Environment variable override (blocks telemetry regardless of consent):

bash
export RTK_TELEMETRY_DISABLED=1

Retention Policy

  • Server-side: telemetry records are retained for a maximum of 12 months, then automatically purged (periodic task every 24 hours).
  • Server-side (erasure log): IP addresses in the erasure audit log are anonymized after 6 months (GDPR — IP is personal data).
  • Client-side: the local SQLite database (~/.local/share/rtk/tracking.db) retains data for 90 days by default (configurable via tracking.history_days in config.toml). Deleted entirely by rtk telemetry forget.

Your Rights (GDPR)

Under the EU General Data Protection Regulation, you have the right to:

  • Access your data: rtk telemetry status shows your device hash; the telemetry payload is fully documented above.
  • Rectification: since data is anonymous and aggregate, rectification is not applicable.
  • Erasure (Art. 17): run rtk telemetry forget to delete local data and send an erasure request to the server. Alternatively, email [email protected] with your device hash.
  • Restriction of processing: rtk telemetry disable stops all data collection immediately.
  • Portability: the local SQLite database at ~/.local/share/rtk/tracking.db contains all locally stored data.
  • Objection: rtk telemetry disable or export RTK_TELEMETRY_DISABLED=1.

Erasure Procedure

  1. Run rtk telemetry forget — this disables telemetry, deletes your device salt, ping marker, and local tracking database (history.db), then sends an erasure request to the server.
  2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email [email protected] for manual erasure.
  3. You can also email [email protected] directly to request manual erasure.

Data Handling

  • Telemetry endpoint URL and auth token are injected at compile time via option_env!() — they are not in the source code
  • All communications use HTTPS (TLS)
  • Data is used exclusively for RTK product improvement
  • No data is sold or shared with third parties
  • Aggregate statistics may be published (e.g. "70% of RTK users are on macOS")

Server-side Requirements

The telemetry server must implement:

  • POST /erasure endpoint accepting {"device_hash": "...", "action": "erasure"}, authenticated via X-RTK-Token
  • Automatic periodic purge of telemetry records older than 12 months
  • Audit log for erasure requests (GDPR Art. 17(2) accountability) with IP anonymization after 6 months

For contributors

The telemetry implementation lives in src/core/telemetry.rs. Key design decisions:

  • Fire-and-forget: errors are silently ignored, never shown to users
  • Non-blocking: runs in a std::thread::spawn, 2-second timeout
  • No async: consistent with RTK's single-threaded design
  • Compile-time gating: if RTK_TELEMETRY_URL is not set at build time, all telemetry code is dead — the binary makes zero network calls
  • 23-hour interval: prevents clock-drift accumulation that a strict 24h interval would cause

When adding new fields:

  1. Add the query method to src/core/tracking.rs
  2. Add the field to EnrichedStats in src/core/telemetry.rs
  3. Populate it in get_enriched_stats()
  4. Add it to the JSON payload in send_ping()
  5. Update this document and the README.md privacy table
  6. Ensure the field contains only aggregate counts or anonymized names — no raw paths, arguments, or user data

Contributing ARCHITECTURE (`docs/contributing/ARCHITECTURE.md`)

rtk Architecture Documentation

Deep reference for RTK's system design, filtering taxonomy, performance characteristics, and architecture decisions. For a guided tour of the end-to-end flow, start with TECHNICAL.md.

rtk (Rust Token Killer) is a high-performance CLI proxy that minimizes LLM token consumption through intelligent output filtering and compression.


Table of Contents

  1. System Overview
  2. Command Lifecycle
  3. Module Organization
  4. Filtering Strategies
  5. Shared Infrastructure
  6. Token Tracking System
  7. Global Flags Architecture
  8. Error Handling
  9. Configuration System
  10. Common Patterns
  11. Build Optimizations
  12. Extensibility Guide
  13. Architecture Decision Records

System Overview

For the proxy pattern diagram and key components table, see TECHNICAL.md.

Design Principles

  1. Single Responsibility: Each module handles one command type
  2. Minimal Overhead: ~5-15ms proxy overhead per command
  3. Exit Code Preservation: CI/CD reliability through proper exit code propagation
  4. Fail-Safe: If filtering fails, fall back to original output
  5. Transparent: Users can always see raw output with -v flags

Hook Architecture (v0.9.5+)

For the hook interception diagram and agent-specific JSON formats, see TECHNICAL.md and hooks/README.md.

Two hook strategies:

text
Auto-Rewrite (default)              Suggest (non-intrusive)
─────────────────────               ────────────────────────
Hook intercepts command             Hook emits systemMessage hint
Rewrites before execution           Claude decides autonomously
Rewrites every matched command      Relies on the model following it
Zero context overhead               Minimal context overhead
Best for: production                Best for: learning / auditing

Command Lifecycle

Six-Phase Execution Flow

text
/* Detailed source-code truncated for AI context efficiency. */

Verbosity Levels

text
-v (Level 1): Show debug messages
  Example: eprintln!("Git log summary:");

-vv (Level 2): Show command being executed
  Example: eprintln!("Executing: git log --oneline -5");

-vvv (Level 3): Show raw output before filtering
  Example: eprintln!("Raw output:\n{}", stdout);

Module Organization

Module Map

For the full file-level module tree, see TECHNICAL.md and each folder's README.

Ecosystems:

text
GIT (cmds/git/)          status, diff, log, gh, gt
  JS/TS (cmds/js/)         lint, tsc, next, prettier, playwright, prisma, vitest, pnpm
  PYTHON (cmds/python/)    ruff, pytest, mypy, pip
  GO (cmds/go/)            go test/build/vet, golangci-lint
  RUBY (cmds/ruby/)        rake, rspec, rubocop
  DOTNET (cmds/dotnet/)    dotnet build/test, binlog
  CLOUD (cmds/cloud/)      aws, docker/kubectl, curl, wget, psql
  SYSTEM (cmds/system/)    ls, tree, read, grep, find, json, log, env, deps
  RUST (cmds/rust/)        cargo test/build/clippy, err

Total: 64 modules (42 command modules + 22 infrastructure modules)

Module Breakdown

  • Command Modules: src/cmds/ — organized by ecosystem (git, rust, js, python, go, dotnet, cloud, system, ruby). Each ecosystem README lists its files.
  • Core Infrastructure: src/core/ — utils, filter, tracking, retriever (recall store), tee, tee_file, config, toml_filter, display_helpers, telemetry, arg_tokenizer
  • Hook System: src/hooks/ — init, rewrite, permissions, hook_cmd, hook_check, hook_audit, verify, trust, integrity
  • Analytics: src/analytics/ — gain, cc_economics, ccusage, session_cmd

Module Count Breakdown

  • Command Modules: 42 (directly exposed to users)
  • Infrastructure Modules: utils, filter, tracking, retriever, tee, tee_file, config, init, gain, toml_filter, verify_cmd, etc.
  • Git Commands: 7 operations (status, diff, log, add, commit, push, branch/checkout)
  • JS/TS Tooling: 8 modules (modern frontend/fullstack development)
  • Python Tooling: 3 modules (ruff, pytest, pip)
  • Go Tooling: 2 modules (go test/build/vet, golangci-lint)

Filtering Strategies

Strategy Matrix

text
/* Detailed source-code truncated for AI context efficiency. */

Code Filtering Levels (src/core/filter.rs)

rust
// FilterLevel::None - Keep everything
fn calculate_total(items: &[Item]) -> i32 {
    // Sum all items
    items.iter().map(|i| i.value).sum()
}

// FilterLevel::Minimal - Strip comments only (20-40% reduction)
fn calculate_total(items: &[Item]) -> i32 {
    items.iter().map(|i| i.value).sum()
}

// FilterLevel::Aggressive - Strip comments + function bodies
fn calculate_total(items: &[Item]) -> i32 { ... }

Language Support: Rust, Python, JavaScript, TypeScript, Go, C, C++, Java

Detection: File extension-based with fallback heuristics


Python & Go Module Architecture

Design Rationale

Added: 2026-02-12 (v0.15.1)
Motivation: Complete language ecosystem coverage beyond JS/TS

Python and Go modules follow distinct architectural patterns optimized for their ecosystems:

text
┌────────────────────────────────────────────────────────────────────────┐
│                 Python vs Go Module Design                             │
└────────────────────────────────────────────────────────────────────────┘

PYTHON (Standalone Commands)         GO (Sub-Enum Pattern)
──────────────────────────           ─────────────────────

Commands::Ruff { args }       ──────  Commands::Go {
Commands::Pytest { args }              Test { args },
Commands::Pip { args }                 Build { args },
                                       Vet { args }
                                     }
├─ ruff_cmd.rs                       Commands::GolangciLint { args }
├─ pytest_cmd.rs                     │
└─ pip_cmd.rs                        ├─ go_cmd.rs (sub-enum router)
                                     └─ golangci_cmd.rs

Mirrors: lint, prettier              Mirrors: git, cargo

Python Stack Architecture

Command Implementations

text
┌────────────────────────────────────────────────────────────────────────┐
│                           Python Commands                              │
└────────────────────────────────────────────────────────────────────────┘

Module            Strategy              Output Format      Savings
─────────────────────────────────────────────────────────────────────────

ruff_cmd.rs       JSON/TEXT DUAL        • check → JSON    80%+
                                        • format → text

  ruff check:  JSON API with structured violations
    {
      "violations": [{"rule": "F401", "file": "x.py", "line": 5}]
    }
    → Group by rule, count occurrences

  ruff format: Text diff output
    "Fixed 12 files"
    → Extract summary, hide unchanged files

pytest_cmd.rs     STATE MACHINE         Text parser       90%+

  State tracking: IDLE → TEST_START → PASSED/FAILED → SUMMARY
  Extract:
    • Test names (test_auth_login)
    • Outcomes (PASSED ✓ / FAILED ✗)
    • Failures only (hide passing tests)

pip_cmd.rs        JSON PARSING          JSON API          70-85%

  pip list --format=json:
    [{"name": "requests", "version": "2.28.1"}]
    → Compact table format

  pip show <pkg>: JSON metadata
    {"name": "...", "version": "...", "requires": [...]}
    → Extract key fields only

  Auto-detect uv: If uv exists, use uv pip instead

Shared Infrastructure

No Package Manager Detection
Unlike JS/TS modules, Python commands don't auto-detect poetry/pipenv/pip because:

  • pip is universally available (system Python)
  • uv detection is explicit (binary presence check)
  • Poetry/pipenv aren't execution wrappers (they manage virtualenvs differently)

Virtual Environment Awareness
Commands respect active virtualenv via sys.executable paths.

Go Stack Architecture

Command Implementations

text
┌────────────────────────────────────────────────────────────────────────┐
│                            Go Commands                                 │
└────────────────────────────────────────────────────────────────────────┘

Module            Strategy              Output Format      Savings
─────────────────────────────────────────────────────────────────────────

go_cmd.rs         SUB-ENUM ROUTER       Mixed formats     75-90%

  go test:  NDJSON STREAMING
    {"Action": "run", "Package": "pkg1", "Test": "TestAuth"}
    {"Action": "fail", "Package": "pkg1", "Test": "TestAuth"}

    → Line-by-line JSON parse (handles interleaved package events)
    → Aggregate: "2 packages, 3 failures (pkg1::TestAuth, ...)"

  go build: TEXT FILTERING
    Errors only (compiler diagnostics)
    → Strip warnings, show errors with file:line

  go vet:   TEXT FILTERING
    Issue detection output
    → Extract file:line:message triples

golangci_cmd.rs   JSON PARSING          JSON API          85%

  golangci-lint run --out-format=json:
    {
      "Issues": [
        {"FromLinter": "errcheck", "Pos": {...}, "Text": "..."}
      ]
    }
    → Group by linter rule, count violations
    → Format: "errcheck: 12 issues, gosec: 5 issues"

Sub-Enum Pattern (go_cmd.rs)

Uses Commands::Go { #[command(subcommand)] command: GoCommand } in main.rs, with GoCommand enum routing to run_test/run_build/run_vet. Mirrors git/cargo patterns.

Why Sub-Enum?

  • go test/build/vet are semantically related (core Go toolchain)
  • Mirrors existing git/cargo patterns (consistency)
  • Natural CLI: rtk go test not rtk gotest

Why golangci-lint Standalone?

  • Third-party tool (not core Go toolchain)
  • Different output format (JSON API vs text)
  • Distinct use case (comprehensive linting vs single-tool diagnostics)

Ruby Module Architecture

Added: 2026-03-15
Motivation: Ruby on Rails development support (minitest, RSpec, RuboCop, Bundler)

Ruby modules follow the standalone command pattern (like Python) with a shared ruby_exec() utility for auto-detecting bundle exec.

text
Module            Strategy              Output Format      Savings
─────────────────────────────────────────────────────────────────────────
rake_cmd.rs       STATE MACHINE         Text parser       85-90%
  Minitest output (rake test / rails test)
  → State machine: Header → Running → Failures → Summary
  → All pass: "ok rake test: 8 runs, 0 failures"
  → Failures: summary + numbered failure details

rspec_cmd.rs      JSON/TEXT DUAL        JSON → 60%+       60%+
  Injects --format json, parses structured results
  → Fallback to text state machine when JSON unavailable
  → Strips Spring, SimpleCov, DEPRECATION, Capybara noise

rubocop_cmd.rs    JSON PARSING          JSON API          60%+
  Injects --format json, groups by cop/severity
  → Skips JSON injection in autocorrect mode (-a, -A)

bundle-install.toml  TOML FILTER       Text rules        90%+
  → Strips "Using" lines, short-circuits to "ok bundle: complete"

Shared: ruby_exec(tool) in utils.rs auto-detects bundle exec when Gemfile exists. Used by rake_cmd, rspec_cmd, rubocop_cmd.

Format Strategy Decision Tree

text
Output format known?
├─ Tool provides JSON flag?
│  ├─ Structured data needed? → Use JSON API
│  │    Examples: ruff check, pip list, golangci-lint
│  │
│  └─ Simple output? → Use text mode
│       Examples: ruff format, go build errors
│
├─ Streaming events (NDJSON)?
│  └─ Line-by-line JSON parse
│       Examples: go test (interleaved packages)
│
└─ Plain text only?
   ├─ Stateful parsing needed? → State machine
   │    Examples: pytest (test lifecycle tracking)
   │
   └─ Simple filtering? → Text filters
        Examples: go vet, go build

Module Integration Checklist

When adding Python/Go module support:

  • Output Format: JSON API > NDJSON > State Machine > Text Filters
  • Failure Focus: Hide passing tests, show failures only
  • Exit Code Preservation: Propagate tool exit codes for CI/CD
  • Virtual Env Awareness: Python modules respect active virtualenv
  • Error Grouping: Group by rule/file for linters (ruff, golangci-lint)
  • Streaming Support: Handle interleaved NDJSON events (go test)
  • Verbosity Levels: Support -v/-vv/-vvv for debug output
  • Token Tracking: Integrate with tracking::track()
  • Unit Tests: Test parsing logic with representative outputs

Shared Infrastructure

Utilities Layer

For the full utilities API (truncate, strip_ansi, execute_command, ruby_exec, etc.), see src/core/README.md. Used by most command modules.

Argument Handling

Every command that inspects its own arguments — which flags were passed, which tokens are paths, what to inject or strip — classifies them with src/core/arg_tokenizer.rs. String scans (starts_with('-'), arg == "--flag") miss a flag's own value, attached values, short clusters and everything past --, which is where this module's whole bug class lives.

For the predicate contract and the four rules that come with it (one grammar per subcommand, scope the lookup to the region the tool parses, inject before the boundary, detect and act with one rule), see src/core/README.md.

Package Manager Detection Pattern

Critical Infrastructure for JS/TS Stack

text
┌────────────────────────────────────────────────────────────────────────┐
│                   Package Manager Detection Flow                       │
└────────────────────────────────────────────────────────────────────────┘

Detection Order:
┌─────────────────────────────────────┐
│ 1. Check: pnpm-lock.yaml exists?   │
│    → Yes: pnpm exec -- <tool>      │
│                                     │
│ 2. Check: yarn.lock exists?        │
│    → Yes: yarn exec -- <tool>      │
│                                     │
│ 3. Fallback: Use npx               │
│    → npx --no-install -- <tool>    │
└─────────────────────────────────────┘

Example (lint_cmd.rs):

let is_pnpm = Path::new("pnpm-lock.yaml").exists();
let is_yarn = Path::new("yarn.lock").exists();

let mut cmd = if is_pnpm {
    Command::new("pnpm").arg("exec").arg("--").arg("eslint")
} else if is_yarn {
    Command::new("yarn").arg("exec").arg("--").arg("eslint")
} else {
    Command::new("npx").arg("--no-install").arg("--").arg("eslint")
};

Affects: lint, tsc, next, prettier, playwright, prisma, vitest, pnpm

Why This Matters:

  • CWD Preservation: pnpm/yarn exec preserve working directory correctly
  • Monorepo Support: Works in nested package.json structures
  • No Global Installs: Uses project-local dependencies only
  • CI/CD Reliability: Consistent behavior across environments

Token Tracking System

SQLite-Based Metrics

text
/* Detailed source-code truncated for AI context efficiency. */

Thread Safety

Single-threaded execution with Mutex<Option> for future-proofing. No multi-threading currently, but safe concurrent access is possible if needed.


Global Flags Architecture

Verbosity System

text
┌────────────────────────────────────────────────────────────────────────┐
│                         Verbosity Levels                               │
└────────────────────────────────────────────────────────────────────────┘

main.rs
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,

Levels:
┌─────────┬──────────────────────────────────────────────────────┐
│ Flag    │ Behavior                                             │
├─────────┼──────────────────────────────────────────────────────┤
│ (none)  │ Compact output only                                  │
│ -v      │ + Debug messages (eprintln! statements)              │
│ -vv     │ + Command being executed                             │
│ -vvv    │ + Raw output before filtering                        │
└─────────┴──────────────────────────────────────────────────────┘

Example (git_cmd.rs):
if verbose > 0 {
    eprintln!("Git diff summary:");
}

Error Handling

anyhow::Result<()> Propagation Chain

text
┌────────────────────────────────────────────────────────────────────────┐
│                      Error Handling Architecture                       │
└────────────────────────────────────────────────────────────────────────┘

Propagation Chain:

main() → Result<()>
  ↓
  match cli.command {
      Commands::Git { args, .. } => git::run(&args, verbose)?,
      ...
  }
  ↓ .context("Git command failed")
git::run(args: &[String], verbose: u8) → Result<()>
  ↓ .context("Failed to execute git")
git::execute_git_command() → Result<String>
  ↓ .context("Git process error")
Command::new("git").output()?
  ↓ Error occurs
anyhow::Error
  ↓ Bubble up through ?
main.rs error display
  ↓
eprintln!("Error: {:#}", err)
  ↓
std::process::exit(1)

Exit Code Preservation (Critical for CI/CD)

text
┌────────────────────────────────────────────────────────────────────────┐
│                    Exit Code Handling Strategy                         │
└────────────────────────────────────────────────────────────────────────┘

Standard Pattern (git_cmd.rs):

let output = Command::new("git").args(args).output()?;

if !output.status.success() {
    let stderr = String::from_utf8_lossy(&output.stderr);
    eprintln!("{}", stderr);
    std::process::exit(output.status.code().unwrap_or(1));
}

Exit Codes:
┌─────────┬──────────────────────────────────────────────────────┐
│ Code    │ Meaning                                              │
├─────────┼──────────────────────────────────────────────────────┤
│ 0       │ Success                                              │
│ 1       │ rtk internal error (parsing, filtering, etc.)        │
│ N       │ Preserved exit code from underlying tool            │
│         │ (e.g., git returns 128, lint returns 1)             │
└─────────┴──────────────────────────────────────────────────────┘

Why This Matters:
• CI/CD pipelines rely on exit codes to determine build success/failure
• Pre-commit hooks need accurate failure signals
• Git workflows require proper exit code propagation

Modules with Exit Code Preservation:
• git_cmd.rs (all git commands)
• lint_cmd.rs (linter failures)
• tsc_cmd.rs (TypeScript errors)
• vitest_cmd.rs (test failures)
• playwright_cmd.rs (E2E test failures)

Configuration System

Configuration

For config file format, recall/tee settings, tracking database path, and TOML filter tiers, see src/core/README.md.

Two tiers: User settings (~/.config/rtk/config.toml) and LLM integration (CLAUDE.md via rtk init).

Initialization Flow

text
┌────────────────────────────────────────────────────────────────────────┐
│                      rtk init Workflow                                 │
└────────────────────────────────────────────────────────────────────────┘

$ rtk init [--global]
      ↓
Check existing CLAUDE.md:
  • --global? → ~/.config/rtk/CLAUDE.md
  • else      → ./CLAUDE.md
      ↓
      ├─ Exists? → Warn user, ask to overwrite
      └─ Not exists? → Continue
      ↓
Prompt: "Initialize rtk for LLM usage? [y/N]"
      ↓ Yes
Write template:
┌─────────────────────────────────────┐
│ # CLAUDE.md                         │
│                                     │
│ Use `rtk` prefix for commands:      │
│ - rtk git status                    │
│ - rtk lint                          │
│ - rtk test                          │
│                                     │
│ Benefits: ≥20% less bash output     │
└─────────────────────────────────────┘
      ↓
Success: "✓ Initialized rtk for LLM integration"

Common Patterns

1. Package Manager Detection (JS/TS modules)

rust
// Detect lockfiles
let is_pnpm = Path::new("pnpm-lock.yaml").exists();
let is_yarn = Path::new("yarn.lock").exists();

// Build command
let mut cmd = if is_pnpm {
    Command::new("pnpm").arg("exec").arg("--").arg("eslint")
} else if is_yarn {
    Command::new("yarn").arg("exec").arg("--").arg("eslint")
} else {
    Command::new("npx").arg("--no-install").arg("--").arg("eslint")
};

2. Verbosity Guards

rust
if verbose > 0 {
    eprintln!("Debug: Processing {} files", count);
}

if verbose >= 2 {
    eprintln!("Executing: {:?}", cmd);
}

if verbose >= 3 {
    eprintln!("Raw output:\n{}", raw);
}

Build Optimizations

Release Profile (Cargo.toml)

toml
[profile.release]
opt-level = 3          # Maximum optimization
lto = true             # Link-time optimization
codegen-units = 1      # Single codegen unit for better optimization
strip = true           # Remove debug symbols

panic = "abort" is deliberately not set: the PostToolUse hook relies on unwinding to fail open when a filter panics.

Performance Targets

  • Startup: <10ms
  • Memory: <5MB in typical usage

Extensibility Guide

For the complete step-by-step process to add a new command (module file, enum variant, routing, tests, documentation), see src/cmds/README.md — Adding a New Command Filter.


Architecture Decision Records

Why Rust?

  • Performance: ~5-15ms overhead per command (negligible for user experience)
  • Safety: No runtime errors from null pointers, data races, etc.
  • Single Binary: No runtime dependencies (distribute one executable)
  • Cross-Platform: Works on macOS, Linux, Windows without modification

Why SQLite for Tracking?

  • Zero Config: No server setup, works out-of-the-box
  • Lightweight: ~100KB database for 90 days of history
  • Reliable: ACID compliance for data integrity
  • Queryable: Rich analytics via SQL (gain report)

Why anyhow for Error Handling?

  • Context: .context() adds meaningful error messages throughout call chain
  • Ergonomic: ? operator for concise error propagation
  • User-Friendly: Error display shows full context chain

Why Clap for CLI Parsing?

  • Derive Macros: Less boilerplate (declarative CLI definition)
  • Auto-Generated Help: --help generated automatically
  • Type Safety: Parse arguments directly into typed structs
  • Global Flags: -v and -u work across all commands

Resources

  • TECHNICAL.md: Guided tour of end-to-end flow
  • CONTRIBUTING.md: Design philosophy, contribution workflow, checklist
  • CLAUDE.md: Quick reference for AI agents (dev commands, build verification)
  • README.md: User guide, installation, examples
  • Cargo.toml: Dependencies, build profiles, package metadata

Glossary

Term Definition
Token Unit of text processed by LLMs (~4 characters on average)
Filtering Reducing output size while preserving essential information
Proxy Pattern rtk sits between user and tool, transforming output
Exit Code Preservation Passing through tool's exit code for CI/CD reliability
Package Manager Detection Identifying pnpm/yarn/npm to execute JS/TS tools correctly
Verbosity Levels -v/-vv/-vvv for progressively more debug output

Last Updated: 2026-03-24
Architecture Version: 3.1


Contributing CODING PRACTICES (`docs/contributing/CODING_PRACTICES.md`)

RTK Coding Practices v1.0

This document follows the Design Philosophy in CONTRIBUTING.md. Once you understand the mental model there, this guide describes the coding practices we use day-to-day in RTK and what reviewers will look for on your PR.

Our goal is to keep the codebase consistent and easy to extend. PRs that deviate from these practices may be asked for changes during review — this is guidance, not a gate. If a rule seems wrong for your specific case, flag it in the PR and we'll discuss.

Heads up: RTK has grown quickly and some code in the repository predates these practices. You may spot modules that don't fully follow them — this is expected, and core/ecosystem maintainers will refactor them over time. When in doubt, follow the practices below for new code rather than mirroring older patterns.


Quick Start for Contributors

New to RTK? The fastest path to a mergeable first PR:

  1. Read the flow once. Start at CONTRIBUTING.md, then skim docs/contributing/TECHNICAL.md to see how a command flows from main.rs → a *_cmd.rs filter → tracking → stdout.
  2. Look at a good example. src/cmds/git/git_cmd.rs is a representative filter — it shows the run() entry point, LazyLock regex setup, filter helpers, and embedded tests all in one file.
  3. Know the shared helpers before reimplementing. Two files cover most of what you need:
    • src/core/runner.rs — command execution wrappers: run_filtered() (run a command, then apply your filter function), run_passthrough() (run unfiltered but tracked), run_streamed() (streaming filter).
    • src/core/utils.rs — shared utilities: resolved_command(), strip_ansi(), truncate(), count_tokens(), and more.
  4. Follow the checklist. src/cmds/README.md — Adding a New Command Filter walks you through creating a filter, registering it, and adding tests.
  5. Write the test first. We follow Red-Green-Refactor. A snapshot test plus a token-savings assertion (see Testing below) is enough for most filters.

If you're unsure whether your approach fits, open a draft PR or a discussion early — we'd rather help shape the design than ask for a rewrite at review.


Design Philosophy

For the full framing (Correctness vs. Token Savings, Transparency, Never Block, Zero Overhead, Extensibility), see the Design Philosophy section in CONTRIBUTING.md.

Two practical reminders that come up often in review:

Portability. RTK should behave the same across platforms. Use #[cfg(target_os = "...")] for platform-specific code; never assume a single OS.

Extensibility. RTK should be modular. Before writing a new feature or filter, check whether an existing entry point fits — runner::run_filtered(), runner::run_passthrough(), helpers in src/core/utils.rs, etc. If your logic could be reused elsewhere, lift it into a shared component rather than burying it in one *_cmd.rs file.


Files, Functions, and Documentation

Each folder contains a root README.md that explains the main principles, flows, and specificities of the source files it owns. These READMEs should describe concepts and cases — not list individual source files or counts, to avoid stale lists as the code evolves. Because the root README reflects core features and logic, it should not change often; meaningful edits usually imply a core refactor.

Tests live in the same file as the code they test (inside #[cfg(test)] mod tests { ... }), not in a separate test file. This keeps the filter, its fixtures, and its assertions close together.


Edge Cases

When you add an edge-case branch or a non-obvious exception, leave a short comment above it explaining why it exists. This prevents a future contributor from removing it because the reason isn't visible from the code alone.

Referencing an issue is often the clearest form:

rust
// ISSUE #123: some `git log` output contains NUL bytes when --format=%x00 is used;
// skip the line rather than panicking on invalid UTF-8.
if line.contains('\0') {
    continue;
}

Comments

Prefer code that reads clearly over code that needs comments to explain it. In particular, avoid redundant comments that restate what the function signature already says.

Comments are welcome when they add information the code cannot carry on its own. The common cases:

  • File header (//!) — purpose and scope of the current file.
  • Edge case — a non-obvious branch or exception, as described above.
  • Issue reference — e.g. // ISSUE #123: the fix for this.
  • "Why, not what" — when the intent or tradeoff behind a decision isn't obvious from the code.

In short: avoid noise comments; keep the ones that would save a future reader a trip to git blame.


Variables

Use explicit, descriptive names for variables, just like for functions.

Do not hardcode repetitive patterns or values that control behavior — extract them into named constants at the top of the file. For anything a user might want to tune (thresholds, limits, display cutoffs), use config::limits() so it flows through ~/.config/rtk/config.toml.

Example from src/cmds/git/git_cmd.rs:

rust
let limits = config::limits();
let max_files = limits.status_max_files;
let max_untracked = limits.status_max_untracked;

Function and File Size

Prefer functions under ~60 lines. Shorter functions are easier to read, test, and reuse. If a function grows beyond that, it's usually a sign the logic should be split into helpers — but this is a guideline, not a hard cap.

Legitimate exceptions include:

  • Dispatcher / match functions that route to subcommands, where each arm delegates to a focused helper.
  • State-machine parsers where splitting would harm readability.

When you keep a longer function, aim to make each block obviously cohesive — and consider leaving a short comment on why splitting it would hurt.

Files are expected to be large in RTK because each module keeps its tests and fixtures alongside the implementation. When a file becomes hard to navigate, split responsibilities across multiple files where possible. If it isn't possible, a big file is acceptable for now.


Imports and Dependencies

RTK is a low-dependency project. Before adding a crate, check whether the functionality is already covered by std, an existing dependency, or src/core/utils.rs. If a few lines of straightforward code will do the job, prefer that over a new dependency.

When a new dependency is genuinely needed, justify it in the PR description. For non-trivial additions, it's worth opening a discussion with maintainers first.


Error Handling

Use anyhow::Result everywhere, and always attach context with .context("description")? or .with_context(|| format!(...)).

Never silently swallow errors (Err(_) => {}). Either log with eprintln! and fall back to raw output (the common case for filters), or propagate the error.

Example of the standard fallback pattern for a filter:

rust
let filtered = filter_output(&output.stdout)
    .unwrap_or_else(|e| {
        eprintln!("rtk: filter warning: {}", e);
        output.stdout.clone() // passthrough on failure — never block the user
    });

For the full error-handling architecture (propagation chain, exit code preservation), see ARCHITECTURE.md — Error Handling.


Testing

See CONTRIBUTING.md — Testing for the full strategy. In short, for a new filter you typically want:

  • Unit + snapshot tests in the same file, using the insta crate.
  • A savings assertion verifying the filter hits the ≥20% target on a real fixture. The target is a reduction in bash output, measured with RTK's token estimator rather than a real tokenizer.

Minimal example:

rust
#[cfg(test)]
mod tests {
    use super::*;
    use insta::assert_snapshot;

    fn count_tokens(s: &str) -> usize { s.split_whitespace().count() }

    #[test]
    fn filter_git_log_snapshot() {
        let input = include_str!("../../../tests/fixtures/git_log_raw.txt");
        let output = filter_git_log(input);
        assert_snapshot!(output);
    }

    #[test]
    fn filter_git_log_savings() {
        let input = include_str!("../../../tests/fixtures/git_log_raw.txt");
        let output = filter_git_log(input);
        let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0);
        assert!(savings >= 20.0, "expected ≥20% output reduction, got {:.1}%", savings);
    }
}

Fixtures go in tests/fixtures/ and should be captured from real command output rather than hand-written.


Security

RTK executes shell commands on behalf of the user, so security is a first-class concern.

Command execution. All commands go through argument arrays via Command::new().args() — never through shell string concatenation. This prevents injection. Always use resolved_command() from src/core/utils.rs instead of a raw Command::new().

Hook integrity. RTK verifies hook files via SHA-256 hashes before operational commands. If a hook has been tampered with, RTK exits with code 1. See src/hooks/integrity.rs.

Project filter trust. .rtk/filters.toml files are not loaded until the user explicitly trusts them, and content changes require re-trust. See src/hooks/trust.rs.

Permission whitelist. is_operational_command() in main.rs uses a whitelist pattern — new commands are not integrity-checked until explicitly added. This is an intentional security posture: fail-open with an audit trail is preferred over false confidence.

unsafe code. Not allowed except for Unix signal handling in proxy mode, which is correctly scoped to #[cfg(unix)].


Contributing TECHNICAL (`docs/contributing/TECHNICAL.md`)

RTK Technical Documentation

Start here for a guided tour of how RTK works end-to-end.

  • CONTRIBUTING.md — Design philosophy, PR process, branch naming, testing requirements
  • ARCHITECTURE.md — Deep reference: filtering taxonomy, performance targets, architecture decisions
  • Each folder has its own README.md with implementation details and file descriptions

1. Project Vision

LLM-powered coding agents (Claude Code, Copilot, Cursor, etc.) consume tokens for every CLI command output they process. Most command outputs contain boilerplate, progress bars, ANSI escape codes, and verbose formatting that wastes tokens without providing actionable information.

RTK sits between the agent and the CLI, filtering outputs to keep only what matters. This reduces the bash output per command, reducing costs and increasing effective context window utilization. RTK is a single Rust binary with no runtime dependencies beyond the compiled binary itself, adding less than 10ms overhead per command.

Every percentage below measures bash output, which is one contributor to input tokens, themselves only part of a bill that also counts output tokens. RTK ships no tokenizer (src/core/tracking.rs estimates bytes / 4), so the ratios are reliable but the absolute token counts are approximate.


2. Architecture Overview

text
/* Detailed source-code truncated for AI context efficiency. */

Design principles:

  • Single-threaded, no async (startup < 10ms)
  • Graceful degradation: filter failure falls back to raw output
  • Exit code propagation: RTK never swallows non-zero exits
  • Transparent proxy: unknown commands pass through unchanged

3. End-to-End Flow

This is the full lifecycle of a command through RTK, from LLM agent to filtered output.

3.1 Hook Installation (`rtk init`)

The user runs rtk init to set up hooks for their LLM agent. This:

  1. Writes a thin shell hook script (e.g., ~/.claude/hooks/rtk-rewrite.sh)
  2. Stores its SHA-256 hash for integrity verification
  3. Patches the agent's settings file (e.g., settings.json) to register the hook
  4. Writes RTK awareness instructions (e.g., RTK.md) for prompt-level guidance

RTK supports 7 agents, each with its own installation mode. The hook scripts are embedded in the binary and written at install time.

Details: src/hooks/README.md covers all installation modes, configuration files, and the uninstall flow.

3.2 Hook Interception (Command Rewriting)

When an LLM agent runs a command (e.g., git status):

  1. The agent fires a PreToolUse event (or equivalent) containing the command as JSON
  2. The hook script reads the JSON, extracts the command string
  3. The hook calls rtk rewrite "git status" as a subprocess
  4. rtk rewrite consults the command registry and returns rtk git status
  5. The hook sends a response telling the agent to use the rewritten command
  6. If anything fails (jq missing, rtk not found, no match), the hook exits silently -- the raw command runs unchanged

All rewrite logic lives in Rust (src/discover/registry.rs). Hooks are thin delegates that handle agent-specific JSON formats.

Details: hooks/README.md covers each agent's JSON format, the rewrite registry, compound command handling, and the RTK_DISABLED override.

Rewrite Pipeline

The rewrite pipeline is how RTK intercepts and rewrites commands. The call chain is:

text
hook shell → rewrite_cmd.rs → rewrite_command() → rewrite_compound() → rewrite_segment() → classify_command()

Traced step by step for cargo fmt --all && cargo test 2>&1 | tail -20:

text
/* Detailed source-code truncated for AI context efficiency. */

Key design decisions:

  • Lexer-based tokenization: A single-pass state machine (lexer.rs) handles all shell constructs (quotes, escapes, redirects, operators). Used for both compound splitting and redirect stripping.
  • Segment-level rewriting: Compound commands are split by operators, each segment rewritten independently. Bash recombines them at execution time.
  • Conservative shell-wrapper recursion: Only exact quoted sh|bash|zsh|fish -c wrappers enter one level of inner rewriting. The parser hands back byte ranges instead of a decoded script, so the wrapper's own bytes are never re-quoted, and it reports the shell it matched so the script is attested under that shell's grammar rather than under bash's.
  • Pipe semantics: Producers and intermediate stages of | remain raw. Only an argument-safe final stage whose rule has pipeline_final_safe may be rewritten; initially this is limited to ordinary grep and rg invocations. Search pattern-file forms (-f/--file) defer because they can consume pipeline stdin as configuration. |& is recognized separately and its complete pipeline stays raw.
  • Double env prefix handling: classify_command() strips env prefixes to match the underlying command against rules. rewrite_segment() extracts the same prefix separately to re-prepend it to the rewritten command.
  • Fallback contract: If any segment fails to match, it stays raw. rewrite_command() returns None only when zero segments were rewritten.

3.3 CLI Parsing and Routing

Once the rewritten command reaches RTK:

  1. Telemetry: telemetry::maybe_ping() fires a non-blocking daily usage ping
  2. Clap parsing: Cli::try_parse() matches against the Commands enum
  3. Hook check: hook_check::maybe_warn() warns if the installed hook is outdated (rate-limited to 1/day)
  4. Integrity check: integrity::runtime_check() verifies the hook's SHA-256 hash for operational commands
  5. Routing: A match cli.command dispatches to the specialized filter module

If Clap parsing fails (command not in the enum), the fallback path runs instead.

3.4 Filter Execution

RTK has two filter systems:

Rust Filters: Compiled modules in src/cmds/ that execute the command, parse its output, and apply specialized transformations (regex, JSON, state machines).

TOML DSL Filters: Declarative filters in src/filters/*.toml that apply regex-based line filtering, truncation, and section extraction. Applied in run_fallback() when no Rust filter matches.

Each filter module follows the same pattern:

  1. Start a timer (TimedExecution::start())
  2. Execute the underlying command (std::process::Command)
  3. Apply filtering (strip boilerplate, group errors, truncate)
  4. On filter error, fall back to raw output
  5. Track token savings to SQLite
  6. Propagate exit code

Details: src/cmds/README.md covers the common pattern, ecosystem organization, cross-command dependencies, and how to add new filters.

3.5 Fallback Path

When Clap parsing fails (unknown command):

  1. Guard: check if the command is an RTK meta-command (gain, init, etc.) -- if so, show Clap error
  2. Look up TOML DSL filters via toml_filter::find_matching_filter()
  3. If TOML match: capture stdout, apply filter pipeline, track savings
  4. If no match: pure passthrough with Stdio::inherit, track as 0% output reduction
text
Command received
  -> Clap parse succeeds?
     -> Yes: Route to Rust filter module
     -> No:  run_fallback()
              -> TOML filter match?
                 -> Yes: Capture stdout, apply filter, track savings
                 -> No:  Passthrough (inherit stdio, track 0% reduction)

Details: src/core/README.md covers the TOML filter engine, filter pipeline stages, and trust-gated project filters.

3.6 Token Tracking

Every command execution records metrics to SQLite (~/.local/share/rtk/tracking.db):

  • Input tokens (raw output size) and output tokens (filtered size)
  • Savings percentage, execution time, project path
  • 90-day automatic retention cleanup
  • Token estimation: ceil(chars / 4.0) approximation

Analytics commands (rtk gain, rtk cc-economics, rtk session) query this database to produce dashboards and ROI reports.

Details: src/analytics/README.md covers the analytics modules, and src/core/README.md covers the tracking database schema.

3.7 Output Recovery (recall store)

On command failure (non-zero exit code) — or when a filter truncates a long list:

  1. Raw unfiltered output is stored in a content-addressed sqlite database (~/.local/share/rtk/recall.db, gzip, byte-faithful)
  2. A hint line is printed: [full output: rtk recall 3f9c2a81d4e7b05a] (failures) or [+N hidden: rtk recall <hash>] (truncated lists)
  3. LLM agents run rtk recall <hash> to get back exactly what was elided instead of re-running the command

The mode is selectable via rtk config recall <sqlite|tee|disabled> — tee keeps the legacy per-file behavior (~/.local/share/rtk/tee/{epoch}_{slug}.log). Recovery never affects command output or exit code. rtk gain --recalls reports how often elided output is actually consulted, per filter.

Details: src/core/README.md covers the recall store, tee mode configuration, and the rotation strategy.


4. Folder Map

Start here, then drill down into each README for file-level details.

`src/` — Rust source code

Directory What it does What you'll find in its README
main.rs CLI entry point, Commands enum, routing match (no README — read the file directly)
core/ Shared infrastructure Tracking DB schema, config system, tee recovery, TOML filter engine, utility functions
hooks/ Hook system Installation flow (rtk init), integrity verification, rewrite command, trust model
analytics/ Token savings analytics rtk gain dashboard, Claude Code economics, ccusage parsing
cmds/ Command filters (9 ecosystems) Common filter pattern, cross-command routing, token savings table, links to each ecosystem
discover/ History analysis + rewrite registry Rewrite patterns, session providers, compound command splitting
learn/ CLI correction detection Error classification, correction pair detection, rule generation
parser/ Parser infrastructure Canonical types (TestResult, LintResult, etc.), 3-tier format modes, migration guide
filters/ TOML filter configs TOML DSL syntax, 8-stage pipeline, inline testing, naming conventions

`hooks/` — Deployed hook artifacts (root directory)

Directory Agent What you'll find in its README
hooks/ (parent) All JSON formats, rewrite registry overview, exit code contract, override controls
claude/ Claude Code Shell hook mechanism, PreToolUse JSON, test script
copilot/ GitHub Copilot Rust binary hook, single PreToolUse schema shared by VS Code Chat and Copilot CLI
cursor/ Cursor IDE Shell hook, empty JSON response requirement
cline/ Cline / Roo Code Rules file (prompt-level, no programmatic hook)
windsurf/ Windsurf / Cascade Rules file (workspace-scoped)
codex/ OpenAI Codex CLI Native PreToolUse processor, hooks.json registration, AGENTS.md awareness
opencode/ OpenCode TypeScript plugin, in-place mutation (OpenCode 2.0 and 1.x >= 1.18.29)

5. Hook System Summary

RTK supports the following LLM agents through hook integrations:

Agent Hook Type Mechanism Can Modify Command?
Claude Code Shell hook PreToolUse in settings.json Yes (updatedInput)
GitHub Copilot (VS Code) Rust binary rtk hook copilot reads JSON Yes (updatedInput)
GitHub Copilot CLI Rust binary rtk hook copilot reads JSON Yes (updatedInput)
Cursor Rust binary rtk hook cursor reads JSON Yes (updated_input)
Gemini CLI Rust binary rtk hook gemini reads JSON Yes (hookSpecificOutput)
Cline/Roo Code Rules file Prompt-level guidance N/A (prompt)
Windsurf Rules file Prompt-level guidance N/A (prompt)
Codex CLI Rust binary rtk hook codex reads JSON Yes (updatedInput)
OpenCode TS plugin execute.before (v2) / tool.execute.before (v1) Yes (in-place mutation)

Details: hooks/README.md has the full JSON schemas for each agent. src/hooks/README.md covers installation, integrity verification, and the rewrite command.


6. Filter Pipeline Summary

Rust Filters (cmds/**)

Compiled filter modules for complex transformations.

Details: src/cmds/README.md and each ecosystem subdirectory README.

TOML DSL Filters (src/filters/*.toml)

Declarative filters with an 8-stage pipeline: strip ANSI, regex replace, match output, strip/keep lines, truncate lines, head/tail, max lines, on-empty message. Loaded from three tiers: built-in (compiled), global (~/.config/rtk/filters/), project-local (.rtk/filters/, trust-gated).

Details: src/core/README.md covers the TOML filter engine.


7. Performance Constraints

Metric Target Verification
Startup time < 10ms hyperfine 'rtk git status' 'git status'
Memory usage < 5MB resident /usr/bin/time -v rtk git status
Binary size < 5MB stripped ls -lh target/release/rtk
Bash output reduction ≥20% per filter (floor) Snapshot + token count tests

Achieved through:

  • Zero async overhead (single-threaded, no tokio)
  • Lazy regex compilation (LazyLock)
  • Minimal allocations (borrow over clone)
  • No config file I/O on startup (loaded on-demand)

8. Testing

Tests live in the module file itself inside a #[cfg(test)] mod tests block (e.g., tests for src/cmds/cloud/container.rs go at the bottom of that same file).

How to Write Tests

1. Create a fixture from real command output (not synthetic data):

bash
kubectl get pods > tests/fixtures/kubectl_pods_raw.txt

2. Write your test in the same module file (#[cfg(test)] mod tests):

rust
#[test]
fn test_my_filter() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    assert!(output.contains("expected content"));
    assert!(!output.contains("noise line"));
}

3. Verify the output reduction (>=20% of the bash output required; count_tokens in tests and the bytes / 4 estimator behind rtk gain are both approximations, reliable as ratios):

rust
#[test]
fn test_my_filter_savings() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0);
    assert!(savings >= 20.0, "Expected >=20% savings, got {:.1}%", savings);
}

Test Organization

text
tests/
├── fixtures/           # Real command output (never synthetic)
│   ├── git_log_raw.txt
│   ├── cargo_test_raw.txt
│   └── dotnet/         # Ecosystem-specific fixtures
└── integration_test.rs # Integration tests (#[ignore])
  • Unit tests: #[cfg(test)] mod tests embedded in each module
  • Fixtures: real command output in tests/fixtures/
  • Integration tests: #[ignore] attribute, run with cargo test --ignored

For testing requirements, pre-commit gate, and PR checklist, see CONTRIBUTING.md — Testing.


9. Future Improvements

  • Extract cli.rs: Move Commands enum, 13 sub-enums (GitCommands, CargoCommands, etc.), and AgentTarget from main.rs to a dedicated cli.rs module. This would reduce main.rs from ~2600 to ~1500 lines.
  • Split routing: Extract the match cli.command { ... } block into a separate routing module.
  • Streaming filters: For long-running commands, filter output line-by-line as it arrives instead of buffering.

Maintainers MAINTAINERS APPLY (`docs/maintainers/MAINTAINERS_APPLY.md`)

RTK Maintainers Application

RTK is growing fast, with more contributors, PRs, and ideas than ever. To keep things moving smoothly, we're looking for new maintainers.

We've introduced two types of maintainers to progressively build a clean process and strong collaboration between contributors.
For now, we're starting by recruiting Ecosystem Maintainers only. As the project evolves, we'll soon begin accepting Core Maintainers as well.

Maintainers are expected to be active and involved over time, not just occasional contributors.


How to apply guide

✅ Requirements

To apply, you should have:

  • 3+ merged PRs to RTK (filters, fixes, docs — all contributions count)
  • 3+ PR reviews with helpful, constructive feedback

✍️ How to Apply

  1. Open a discussion in rtk-ai/rtk Maintainers Applications · Discussions · GitHub titled Maintainer Application: [Your GitHub Handle]
  2. In your application, include:
    • The ecosystem(s) you're interested in
    • Your experience with those ecosystems
    • Links to your merged PRs and reviews
    • Your Discord username (and make sure you've joined the server)
    • Your PRs that have been accepted in RTK
  3. For Core Maintainer applications, also include:
    • Your experience with Rust
    • Your experience with Open Source
  4. A Core Maintainer will get back to you as soon as possible
  5. If it's a good fit, we'll continue the conversation on Discord and guide you through the next steps

👀 What to Expect

  • A review of your ecosystem experience and understanding of RTK concepts
  • A discussion with current maintainers
  • Introduction to the team

What Maintainers Do

🌱 Ecosystem Maintainers

Ecosystem Maintainers are responsible for specific environments inside the cmds/ folder (e.g. git, system, etc.). They own and manage their ecosystem end-to-end:

  • Responsible for the quality of filters
  • Review and ensure quality of contributions
  • Maintain consistency with the rest of the RTK ecosystem
  • Help shape and grow their specific domain
  • Handle issues and PRs related to their environment (security and quality review from core maintainers still required for release)

🔧 Core Maintainers (once we've fully integrated some Ecosystem Maintainers)

Core Maintainers are responsible for the core of RTK. They have a broader scope and higher responsibilities and permissions, including:

  • Maintaining core functionalities and architecture
  • Reviewing and merging PRs for release with the core team
  • Defining project direction and standards with the core team
  • Ensuring consistency across the entire project
  • Refactoring for optimization, standardization & conformity

If you enjoy contributing and want to help RTK scale in a healthy way, we'd be excited to have you onboard 🚀


Usage AUDIT GUIDE (`docs/usage/AUDIT_GUIDE.md`)

RTK Token Savings Audit Guide

Complete guide to analyzing your rtk token savings with temporal breakdowns and data exports.

Overview

The rtk gain command provides comprehensive analytics for tracking your token savings across time periods.

Database Location: ~/.local/share/rtk/history.db
Retention Policy: 90 days
Scope: Global across all projects, worktrees, and Claude sessions

Quick Reference

bash
# Default summary view
rtk gain

# Temporal breakdowns
rtk gain --daily          # All days since tracking started
rtk gain --weekly         # Aggregated by week
rtk gain --monthly        # Aggregated by month
rtk gain --all            # Show all breakdowns at once

# Export formats
rtk gain --all --format json > savings.json
rtk gain --all --format csv > savings.csv

# Combined flags
rtk gain --graph --history --quota    # Classic view with extras
rtk gain --daily --weekly --monthly   # Multiple breakdowns

# Reset all tracking data
rtk gain --reset          # prompts [y/N] before deleting
rtk gain --reset --yes    # skip prompt (CI/scripts)

Command Options

Temporal Flags

Flag Description Output
--daily Day-by-day breakdown All days with full metrics
--weekly Week-by-week breakdown Aggregated by Sunday-Saturday weeks
--monthly Month-by-month breakdown Aggregated by calendar month
--all All time breakdowns Daily + Weekly + Monthly combined

Classic Flags (still available)

Flag Description
--graph ASCII graph of last 30 days
--history Recent 10 commands
--quota Monthly quota analysis (Pro/5x/20x tiers)
--tier <TIER> Quota tier: pro, 5x, 20x (default: 20x)

Reset Flag

Flag Description
--reset Permanently delete all tracking data (commands + parse failures)
--yes Skip the confirmation prompt (for CI/scripts)

Warning: --reset is irreversible. It clears both the commands and parse_failures tables atomically. A [y/N] confirmation prompt is shown by default. In non-interactive environments (piped stdin), it defaults to N unless --yes is passed.

Export Formats

Format Flag Use Case
text --format text (default) Terminal display
json --format json Programmatic analysis, APIs
csv --format csv Excel, data analysis, plotting

Output Examples

All values below are illustrative.

Daily Breakdown

text
📅 Daily Breakdown (3 days)
════════════════════════════════════════════════════════════════
Date            Cmds      Input     Output      Saved   Save%
────────────────────────────────────────────────────────────────
2026-01-01       100     500.0K     100.0K     400.0K   80.0%
2026-01-02       100     500.0K     100.0K     400.0K   80.0%
2026-01-03       100     500.0K     100.0K     400.0K   80.0%
────────────────────────────────────────────────────────────────
TOTAL            300       1.5M     300.0K       1.2M   80.0%

Metrics explained:

  • Cmds: Number of rtk commands executed
  • Input: Estimated tokens from raw command output
  • Output: Estimated tokens after rtk filtering
  • Saved: Input - Output (tokens prevented from reaching LLM)
  • Save%: Percentage reduction (Saved / Input × 100)

Weekly Breakdown

text
📊 Weekly Breakdown (1 weeks)
════════════════════════════════════════════════════════════════════════
Week                      Cmds      Input     Output      Saved   Save%
────────────────────────────────────────────────────────────────────────
12-28 → 01-03              300       1.5M     300.0K       1.2M   80.0%
────────────────────────────────────────────────────────────────────────
TOTAL                      300       1.5M     300.0K       1.2M   80.0%

Week definition: Sunday to Saturday (ISO week starting Sunday at 00:00)

Monthly Breakdown

text
📆 Monthly Breakdown (1 months)
════════════════════════════════════════════════════════════════
Month         Cmds      Input     Output      Saved   Save%
────────────────────────────────────────────────────────────────
2026-01        300       1.5M     300.0K       1.2M   80.0%
────────────────────────────────────────────────────────────────
TOTAL          300       1.5M     300.0K       1.2M   80.0%

Month format: YYYY-MM (calendar month)

JSON Export

json
{
  "summary": {
    "total_commands": 300,
    "total_input": 1500000,
    "total_output": 300000,
    "total_saved": 1200000,
    "avg_savings_pct": 80.0
  },
  "daily": [
    {
      "date": "2026-01-01",
      "commands": 100,
      "input_tokens": 500000,
      "output_tokens": 100000,
      "saved_tokens": 400000,
      "savings_pct": 80.0
    }
  ],
  "weekly": [...],
  "monthly": [...]
}

Use cases:

  • API integration
  • Custom dashboards
  • Automated reporting
  • Data pipeline ingestion

CSV Export

csv
# Daily Data
date,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2026-01-01,100,500000,100000,400000,80.0
2026-01-02,100,500000,100000,400000,80.0

# Weekly Data
week_start,week_end,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2025-12-28,2026-01-03,300,1500000,300000,1200000,80.0

# Monthly Data
month,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2026-01,300,1500000,300000,1200000,80.0

Use cases:

  • Excel analysis
  • Python/R data science
  • Google Sheets dashboards
  • Matplotlib/seaborn plotting

Analysis Workflows

Weekly Progress Tracking

bash
# Generate weekly report every Monday
rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv

# Compare this week vs last week
rtk gain --weekly | tail -3

Monthly Cost Analysis

bash
# Export monthly data for budget review
rtk gain --monthly --format json | jq '.monthly[] |
  {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}'

Data Science Analysis

python
import pandas as pd
import subprocess

# Get CSV data
result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'],
                       capture_output=True, text=True)

# Parse daily data
lines = result.stdout.split('\n')
daily_start = lines.index('# Daily Data') + 2
daily_end = lines.index('', daily_start)
daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end])))

# Plot savings trend
daily_df['date'] = pd.to_datetime(daily_df['date'])
daily_df.plot(x='date', y='savings_pct', kind='line')

Excel Analysis

  1. Export CSV: rtk gain --all --format csv > rtk-data.csv
  2. Open in Excel
  3. Create pivot tables:
    • Daily trends (line chart)
    • Weekly totals (bar chart)
    • Savings % distribution (histogram)

Dashboard Creation

bash
# Generate dashboard data daily via cron
0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json

# Serve with static site
cat > index.html <<'EOF'
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="savings"></canvas>
<script>
fetch('rtk-stats.json')
  .then(r => r.json())
  .then(data => {
    new Chart(document.getElementById('savings'), {
      type: 'line',
      data: {
        labels: data.daily.map(d => d.date),
        datasets: [{
          label: 'Daily Savings %',
          data: data.daily.map(d => d.savings_pct)
        }]
      }
    });
  });
</script>
EOF

Understanding Token Savings

Token Estimation

rtk gain estimates tokens as bytes / 4; RTK ships no tokenizer. The same estimator is applied to raw and filtered output, so the percentage is reliable; the absolute token counts are approximate and will not match your provider's billing.

Savings Calculation

text
Input Tokens    = estimate_tokens(raw_command_output)
Output Tokens   = estimate_tokens(rtk_filtered_output)
Saved Tokens    = Input - Output
Savings %       = (Saved / Input) × 100

Savings % is a bash output byte ratio. Those bytes are one contributor to input tokens, and input tokens are only part of a bill that also counts output tokens.

Database Management

Inspect Raw Data

bash
# Location
ls -lh ~/.local/share/rtk/history.db

# Schema
sqlite3 ~/.local/share/rtk/history.db ".schema"

# Recent records
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"

# Total database size
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT COUNT(*),
          SUM(saved_tokens) as total_saved,
          MIN(DATE(timestamp)) as first_record,
          MAX(DATE(timestamp)) as last_record
   FROM commands"

Backup & Restore

bash
# Backup
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db

# Restore
cp ~/backups/rtk-history-YYYYMMDD.db ~/.local/share/rtk/history.db

# Export for migration
sqlite3 ~/.local/share/rtk/history.db .dump > rtk-backup.sql

Cleanup

bash
# Manual cleanup (older than 90 days)
sqlite3 ~/.local/share/rtk/history.db \
  "DELETE FROM commands WHERE timestamp < datetime('now', '-90 days')"

# Reset all data
rm ~/.local/share/rtk/history.db
# Next rtk command will recreate database

Integration Examples

GitHub Actions CI/CD

yaml
# .github/workflows/rtk-stats.yml
name: RTK Stats Report
on:
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday
jobs:
  stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install rtk
        run: cargo install --git https://github.com/rtk-ai/rtk rtk
      - name: Generate report
        run: |
          rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json
      - name: Commit stats
        run: |
          git add stats/
          git commit -m "Weekly rtk stats"
          git push

Slack Bot

python
import subprocess
import json
import requests

def send_rtk_stats():
    result = subprocess.run(['rtk', 'gain', '--format', 'json'],
                           capture_output=True, text=True)
    data = json.loads(result.stdout)

    message = f"""
    📊 *RTK Token Savings Report*

    Total Saved: {data['summary']['total_saved']:,} tokens
    Savings Rate: {data['summary']['avg_savings_pct']:.1f}%
    Commands: {data['summary']['total_commands']}
    """

    requests.post(SLACK_WEBHOOK_URL, json={'text': message})

Troubleshooting

No data showing

bash
# Check if database exists
ls -lh ~/.local/share/rtk/history.db

# Check record count
sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands"

# Run a tracked command to generate data
rtk git status

Export fails

bash
# Check for pipe errors
rtk gain --format json 2>&1 | tee /tmp/rtk-debug.log | jq .

Incorrect statistics

Token estimation is a heuristic. For precise measurements:

bash
# Install tiktoken
pip install tiktoken

# Validate estimation
rtk git status > output.txt
python -c "
import tiktoken
enc = tiktoken.get_encoding('cl100k_base')
text = open('output.txt').read()
print(f'Actual tokens: {len(enc.encode(text))}')
print(f'rtk estimate: {len(text) // 4}')
"

Best Practices

  1. Regular Exports: rtk gain --all --format json > monthly-$(date +%Y%m).json
  2. Trend Analysis: Compare week-over-week savings to identify optimization opportunities
  3. Command Profiling: Use --history to see which commands save the most
  4. Backup Before Cleanup: Always backup before manual database operations
  5. CI Integration: Track savings across team in shared dashboards

See Also


Usage FEATURES (`docs/usage/FEATURES.md`)

RTK - Complete Feature Reference

rtk (Rust Token Killer) -- High-performance CLI proxy that cuts up to 90% of the bash output your agent reads.

Single Rust binary, zero external dependencies, < 10ms overhead per command.

About bash output reduction

The savings RTK reports (for example in rtk gain) measure bash output bytes removed: the output a shell command returns before the agent reads it. It is not equivalent to a bill reduction of the same size.

Bash output is only one source of input tokens, alongside your prompt, the system prompt and the conversation history. Input tokens are themselves only part of the bill, which also counts output tokens. The reduction dilutes at each step.

The token counts RTK displays are estimated as bytes / 4: RTK ships no tokenizer, so the percentages are reliable but absolute token counts are approximate.

Full explanation: How RTK Savings Work


Table of contents

  1. Overview
  2. Global flags
  3. File commands
  4. Git commands
  5. GitHub CLI commands
  6. Test commands
  7. Build and lint commands
  8. Formatting commands
  9. Package managers
  10. Containers and orchestration
  11. Data and network
  12. Cloud and databases
  13. Stacked PRs (Graphite)
  14. Analytics and tracking
  15. Hook system
  16. Configuration
  17. Recall system (output recovery)
  18. Telemetry

Overview

rtk acts as a proxy between an LLM (Claude Code, Gemini CLI, etc.) and system commands. Four filtering strategies are applied depending on the command type:

Strategy Description Example
Smart filtering Removes noise (comments, whitespace, boilerplate) ls -la -> compact tree
Grouping Aggregates by directory, error type, rule Tests grouped by file
Truncation Keeps relevant context, removes redundancy Condensed diff
Deduplication Merges repeated log lines with counters error x42

Fallback mechanism

If rtk does not recognize a subcommand, it runs the raw command (passthrough) and records the event in the tracking database. This makes rtk always safe to use -- no command is ever blocked.


Global flags

These flags apply to all subcommands:

Flag Short Description
--verbose -v Increase verbosity (-v, -vv, -vvv). Shows filtering details.
--skip-env -- Sets SKIP_ENV_VALIDATION=1 for child processes (Next.js, tsc, lint, prisma).

Examples:

bash
rtk -v git status          # Compact status + filtering details on stderr
rtk -vvv cargo test        # Maximum verbosity (debug)
rtk --skip-env next build  # Disable Next.js env validation

File commands

`rtk ls` -- Directory listing

Purpose: Replaces ls and tree with token-optimized output.

Syntax:

bash
rtk ls [args...]

All native ls flags are supported (-l, -a, -h, -R, etc.).

Before / After:

text
# ls -la (45 lines, ~800 tokens)           # rtk ls (12 lines, ~150 tokens)
drwxr-xr-x  15 user staff 480 ...          my-project/
-rw-r--r--   1 user staff 1234 ...          +-- src/ (8 files)
-rw-r--r--   1 user staff 567 ...           |   +-- main.rs
...40 more lines...                         +-- Cargo.toml
                                            +-- README.md

`rtk tree` -- Directory tree

Purpose: Proxy to native tree with filtered output.

Syntax:

bash
rtk tree [args...]

Supports all native tree flags (-L, -d, -a, etc.).


`rtk read` -- File reading

Purpose: Replaces cat, head, tail with smart content filtering.

Syntax:

bash
rtk read <file> [options]
rtk read - [options]          # Read from stdin

Options:

Option Short Default Description
--level -l minimal Filtering level: none, minimal, aggressive
--max-lines -m unlimited Structural preview capped at N lines (signatures and imports, not the first N)
--head-lines unlimited Keep only the first N lines, byte-exact
--tail-lines unlimited Keep only the last N lines, byte-exact
--line-numbers -n no Show line numbers

Filtering levels:

Level Description
none No filtering, raw output
minimal Removes comments and excessive blank lines
aggressive Signatures only (removes function bodies)

Before / After (aggressive mode):

text
# cat main.rs (~200 lines)                  # rtk read main.rs -l aggressive (~50 lines)
fn main() -> Result<()> {                   fn main() -> Result<()> { ... }
    let config = Config::load()?;           fn process_data(input: &str) -> Vec<u8> { ... }
    let data = process_data(&input);        struct Config { ... }
    for item in data {                      impl Config { fn load() -> Result<Self> { ... } }
        println!("{}", item);
    }
    Ok(())
}
...

Languages supported for filtering: Rust, Python, JavaScript, TypeScript, Go, C, C++, Java, Ruby, Shell.


`rtk smart` -- Heuristic summary

Purpose: Generates a 2-line technical summary of a source file.

Syntax:

bash
rtk smart <file> [--model heuristic] [--force-download]

Example:

text
$ rtk smart src/tracking.rs
SQLite-based token tracking system for command executions.
Records input/output tokens, savings %, execution times with 90-day retention.

Purpose: Replaces find and fd with compact output grouped by directory.

Syntax:

bash
rtk find [args...]

Supports both RTK syntax and native find syntax (-name, -type, etc.).

Before / After:

text
# find . -name "*.rs" (30 lines)            # rtk find "*.rs" . (8 lines)
./src/main.rs                                src/ (12 .rs)
./src/git.rs                                   main.rs, git.rs, config.rs
./src/config.rs                                tracking.rs, filter.rs, utils.rs
./src/tracking.rs                              ...6 more
./src/filter.rs                              tests/ (3 .rs)
./src/utils.rs                                 test_git.rs, test_ls.rs, test_filter.rs
...24 more lines...

Purpose: Replaces grep and rg with truncated output grouped by file.

Syntax:

bash
rtk grep <pattern> [path] [options]

Options:

Option Short Default Description
--max-len 80 Maximum line length
--max 200 Maximum number of results
--context-only no Show only the match context

None of these options has a short form: -l, -m and -c are reserved for the native flags with the same letter (--files-with-matches, --max-count, --count) and are passed to the engine.

Extra arguments are passed to the engine actually invoked -- grep for rtk grep, rg for rtk rg; RTK never substitutes one for the other. A ripgrep-specific flag (--glob, -t) is therefore only valid under rtk rg. Flags that change the output format (-c, -l, -L, -o, -Z) go straight to the engine without RTK filtering.

There is no --file-type option: rtk rg -t rust filters by type, while rtk grep -t rust surfaces grep's error (invalid option -- 't').

Before / After:

text
# grep -rn "fn run" (20 lines)              # rtk grep -rn "fn run" (10 lines)
src/git.rs:45:pub fn run(...)                src/git.rs
src/git.rs:120:fn run_status(...)              45: pub fn run(...)
src/ls.rs:12:pub fn run(...)                   120: fn run_status(...)
src/ls.rs:25:fn run_tree(...)                src/ls.rs
...                                            12: pub fn run(...)
                                               25: fn run_tree(...)

Purpose: Replaces ast-grep with capped output grouped by file.

Syntax:

bash
rtk ast-grep run -p '<pattern>' [path] [options]

Groups matches by file, capped at 5 per file and 50 in total; the overflow is replaced by a count note ("N more match line(s) in X" / "N more match line(s) in M more file(s) not shown"), followed by a [full output: ...] hint pointing to the complete output, as long as recall is enabled ([retriever] mode) and the compaction saves enough to pay for the hint. Otherwise the count note is followed by a fallback (--json, a narrower pattern, or rtk proxy ast-grep), and the full output is not archived. When the compacted output would end up costing more than the raw one (count notes that cost more than the lines they replace, or a hint or fallback longer than the margin saved), the raw output is printed whole, with no note and no fallback. ast-grep prints one line per source line of a match, and a structural match can span several lines, so the count is in lines, not matches. On a real search in this repository, ~85% reduction.

Only run is filtered: either named explicitly, or implied when no positional argument before -- names another subcommand. scan, test, new, lsp, outline, completions and help pass through unchanged (the ast-grep 0.45.3 subcommands other than run): their output does not have this shape, and lsp talks over stdin. So do run --stdin and run -i: the first reads the source on the pipe the capture closes, the second opens a full-screen session (one question per match) that it writes to stdout while reading answers from /dev/tty, so the capture would swallow the whole session, question included, while ast-grep waits for an answer.

--json is not filtered -- an explicit request for structured output passes through unchanged, without compression.


`rtk diff` -- Condensed diff

Purpose: Ultra-condensed diff between two files (changed lines only).

Syntax:

bash
rtk diff <file1> <file2>
rtk diff -                    # Condense a unified diff read on stdin

Pour comparer deux fichiers : code de sortie 0 si identiques, 1 si differents,
2 si un fichier ne peut pas etre lu. Les fichiers non UTF-8 sont compares octet
par octet ; seuls leurs noms sont affiches lorsqu'ils different.


`rtk wc` -- Compact count

Purpose: Replaces wc with compact output (removes paths and padding).

Syntax:

bash
rtk wc [args...]

Supports all native wc flags (-l, -w, -c, etc.).


Git commands

Overview

All git subcommands are supported. Unrecognized commands are passed directly to git (passthrough).

Global git options:

Option Description
-C Change directory before running
-c <key=value> Override a git config value
--git-dir Path to the .git directory
--work-tree Path to the working tree
--no-pager Disable the pager
--no-optional-locks Skip optional locks
--bare Treat as a bare repository
--literal-pathspecs Literal pathspecs

`rtk git status` -- Compact status

bash
rtk git status [args...]    # Supports all git status flags

Before / After:

text
# git status (~20 lines, ~400 tokens)       # rtk git status (~5 lines, ~80 tokens)
On branch main                               main | 3M 1? 1A
Your branch is up to date with               M src/main.rs
  'origin/main'.                              M src/git.rs
                                              M tests/test_git.rs
Changes not staged for commit:                ? new_file.txt
  (use "git add <file>..." to update)        A staged_file.rs
  modified:   src/main.rs
  modified:   src/git.rs
  ...

`rtk git log` -- Compact history

bash
rtk git log [args...]    # Supports --oneline, --graph, --all, -n, etc.

Before / After:

text
# git log (50+ lines)                       # rtk git log -n 5 (5 lines)
commit abc123def... (HEAD -> main)           abc123 Fix token counting bug
Author: User <[email protected]>               def456 Add vitest support
Date:   Mon Jan 15 10:30:00 2024            789abc Refactor filter engine
                                             012def Update README
    Fix token counting bug                   345ghi Initial commit
...

`rtk git diff` -- Compact diff

bash
rtk git diff [args...]    # Supports --stat, --cached, --staged, etc.

Before / After:

Raw git diff:

text
diff --git a/git.rs b/git.rs
index 50f7a19..225f918 100644
--- a/git.rs
+++ b/git.rs
@@ -1,3 +1,3 @@
 fn helper(branch: &str) -> String {
-    format!("ko {}", branch)
+    format!("ok {}", branch)
 }
diff --git a/main.rs b/main.rs
index 765936a..7abfb4f 100644
--- a/main.rs
+++ b/main.rs
@@ -1,5 +1,5 @@
 fn main() {
-    let x = 42;
-    // old code
+    let config = Config::load()?;
+    config.validate()?;
     println!("start");
 }

rtk git diff:

text
git.rs  | 2 +-
 main.rs | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

Changes:

git.rs
@@ -1,3 +1,3 @@
 fn helper(branch: &str) -> String {
-    format!("ko {}", branch)
+    format!("ok {}", branch)
 }
  +1 -1

main.rs
@@ -1,5 +1,5 @@
 fn main() {
-    let x = 42;
-    // old code
+    let config = Config::load()?;
+    config.validate()?;
     println!("start");
 }
  +2 -2

On a diff this small, rtk saves little. Compression comes from larger diffs, where per-file headers and per-hunk caps matter; truncated output ends with a truncation note and the hint [full diff: rtk git diff --no-compact].

Hunk lines and @@ headers are printed at column 0, in git's unified form, so rtk git diff | grep "^-" works. rtk's own annotations are indented by two spaces so those same greps do not count them: the per-file total ( +2 -2) and the truncation note ( ... (30 deletions, 20 additions truncated)).

Limitations:

  • Beyond 100 changed lines per hunk, grep -c "^-" is capped at what is displayed; the truncation note gives the remainder per sign, but it is indented and therefore invisible to the same grep.
  • On a combined diff (diff --cc, which git produces for each conflicted path during a merge, rebase, cherry-pick or stash pop), the marker can sit in the second column: +ours is counted in the +N -M total but stays invisible to grep "^+".
  • The output is not an applicable patch: rtk removes the diff --git / --- / +++ headers. Use rtk bypass git diff for a faithful patch, or rtk gh pr diff --patch, which passes through unfiltered.

`rtk git show` -- Compact show

bash
rtk git show [args...]

Shows the commit summary + stat + compact diff.

Warning (redirecting to a file). For a large blob
(rtk git show HEAD:large-file), the output is windowed: only a preview
is shown, followed by a hint
[see remaining: rtk proxy git show 'HEAD:...' | tail -n +N].
A hand-written rtk git show HEAD:x > file can therefore silently
truncate the content (the exit code stays 0). To capture the full
file, follow the recovery hint, or use rtk proxy git show. A bare
git show is not enough when the RTK hook is active: it is rewritten to
rtk git show, which windows the output again.


`rtk git add` -- Compact add

bash
rtk git add [args...]    # Supports -A, -p, --all, etc.

Output: ok (a single word)


`rtk git commit` -- Compact commit

bash
rtk git commit -m "message" [args...]    # Supports -a, --amend, --allow-empty, etc.

Output: ok abc1234 (confirmation + short hash)


`rtk git push` -- Compact push

bash
rtk git push [args...]    # Supports -u, remote, branch, etc.

Before / After:

text
# git push (15 lines, ~200 tokens)          # rtk git push (1 line, ~10 tokens)
Enumerating objects: 5, done.                ok main
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
...

`rtk git pull` -- Compact pull

bash
rtk git pull [args...]

Output: ok 3 files +10 -2


`rtk git branch` -- Compact branches

bash
rtk git branch [args...]    # Supports -d, -D, -m, etc.

Shows the current branch, local branches and remote branches compactly.


`rtk git fetch` -- Compact fetch

bash
rtk git fetch [args...]

Output: ok fetched (N new refs)


`rtk git stash` -- Compact stash

bash
rtk git stash [list|show|pop|apply|drop|push] [args...]

`rtk git worktree` -- Compact worktree

bash
rtk git worktree [add|remove|prune|list] [args...]

Git passthrough

Any git subcommand not listed above is run directly:

bash
rtk git rebase main        # Runs git rebase main
rtk git cherry-pick abc    # Runs git cherry-pick abc
rtk git tag v1.0.0         # Runs git tag v1.0.0

GitHub CLI commands

`rtk gh` -- Compact GitHub CLI

Purpose: Replaces gh with optimized output.

Syntax:

bash
rtk gh <subcommand> [args...]

Supported subcommands:

Command Description
rtk gh pr list Compact PR list
rtk gh pr view <num> PR details + checks
rtk gh pr checks CI check status
rtk gh issue list Compact issue list
rtk gh run list Workflow run status
rtk gh api <endpoint> Compact API response

Before / After:

text
# gh pr list (~30 lines)                    # rtk gh pr list (~10 lines)
Showing 10 of 15 pull requests in org/repo   #42 feat: add vitest (open, 2d)
                                              #41 fix: git diff crash (open, 3d)
#42  feat: add vitest support                 #40 chore: update deps (merged, 5d)
  user opened about 2 days ago                #39 docs: add guide (merged, 1w)
  ... labels: enhancement
...

Test commands

`rtk test` -- Generic test wrapper

Purpose: Runs any test command and shows only the failures.

Syntax:

bash
rtk test <command...>
rtk test --shell fish '<fish command>'

By default the command and its arguments are run directly, with no shell
expansion. --shell takes a whole command as a single argument when a shell's
syntax is needed.

Example:

bash
rtk test cargo test
rtk test npm test
rtk test bun test
rtk test pytest

Before / After:

text
# cargo test (200+ lines on failure)        # rtk test cargo test (~20 lines)
running 15 tests                             FAILED: 2/15 tests
test utils::test_parse ... ok                  test_edge_case: assertion failed
test utils::test_format ... ok                 test_overflow: panic at utils.rs:18
test utils::test_edge_case ... FAILED
...150 lines of backtrace...

`rtk err` -- Errors/warnings only

Purpose: Runs a command and shows only errors and warnings.

Syntax:

bash
rtk err <command...>
rtk err --shell fish '<fish command>'

Without --shell, argument boundaries are preserved, and globs, variables and
operators are not interpreted by a shell.

Example:

bash
rtk err npm run build
rtk err cargo build

`rtk cargo test` -- Rust tests

bash
rtk cargo test [args...]

Shows only failures. Supports all cargo test arguments.


`rtk cargo nextest` -- Rust tests (nextest)

bash
rtk cargo nextest [run|list|--lib] [args...]

Filters cargo nextest output to show only failures.


`rtk jest` / `rtk vitest` -- Jest/Vitest tests

bash
rtk jest [args...]
rtk vitest [args...]

`rtk playwright test` -- Playwright E2E tests

bash
rtk playwright [args...]

`rtk pytest` -- Python tests

bash
rtk pytest [args...]

`rtk go test` -- Go tests

bash
rtk go test [args...]

Uses Go's NDJSON streaming output for precise filtering.


Build and lint commands

`rtk cargo build` -- Rust build

bash
rtk cargo build [args...]

Removes "Compiling..." lines, keeping only errors and the final result.


`rtk cargo check` -- Rust check

bash
rtk cargo check [args...]

Removes "Checking..." lines, keeping only errors.


`rtk cargo clippy` -- Rust Clippy

bash
rtk cargo clippy [args...]

Groups warnings by lint rule.


`rtk cargo install` -- Rust install

bash
rtk cargo install [args...]

Removes dependency compilation, keeping only the install result and errors.


`rtk tsc` -- TypeScript compiler

bash
rtk tsc [args...]

Groups TypeScript errors by file and error code.

Before / After:

text
# tsc --noEmit (50 lines)                   # rtk tsc (15 lines)
src/api.ts(12,5): error TS2345: ...          src/api.ts (3 errors)
src/api.ts(15,10): error TS2345: ...           TS2345: Argument type mismatch (x2)
src/api.ts(20,3): error TS7006: ...            TS7006: Parameter implicitly has 'any'
src/utils.ts(5,1): error TS2304: ...         src/utils.ts (1 error)
...                                            TS2304: Cannot find name 'foo'

`rtk lint` -- ESLint / Biome

bash
rtk lint [args...]
rtk lint biome [args...]

Groups violations by rule and by file. Auto-detects the linter.


`rtk prettier` -- Format checking

bash
rtk prettier [args...]    # e.g. rtk prettier --check .

Shows only the files that need formatting.


`rtk format` -- Universal formatter

bash
rtk format [args...]

Auto-detects the project formatter (prettier, black, ruff format) and applies a compact filter.


`rtk next build` -- Next.js build

bash
rtk next [args...]

Compact output with route metrics.


`rtk ruff` -- Python linter/formatter

bash
rtk ruff check [args...]
rtk ruff format --check [args...]

Compressed JSON output.


`rtk mypy` -- Python type checker

bash
rtk mypy [args...]

Groups type errors by file.


`rtk golangci-lint` -- Go linter

bash
rtk golangci-lint run [args...]

Compressed JSON output.


Formatting commands

`rtk prettier` -- Prettier

bash
rtk prettier --check .
rtk prettier --write src/

`rtk format` -- Universal detector

bash
rtk format [args...]

Automatically detects prettier, black, ruff format and rustfmt. Applies a unified compact filter.


Package managers

`rtk pnpm` -- pnpm

Command Description
rtk pnpm list [-d N] Compact dependency tree
rtk pnpm outdated Outdated packages: pkg: old -> new
rtk pnpm install Filters progress bars
rtk pnpm build Delegates to the Next.js filter
rtk pnpm typecheck Delegates to the tsc filter

Unrecognized subcommands are passed directly to pnpm (passthrough).


`rtk npm` -- npm

bash
rtk npm [args...]    # e.g. rtk npm run build

Filters npm boilerplate (progress bars, headers, etc.).


`rtk npx` -- npx with smart routing

bash
rtk npx [args...]

Routes to specialized filters:

  • rtk npx tsc -> tsc filter
  • rtk npx eslint -> lint filter
  • rtk npx prisma -> prisma filter
  • Others -> filtered passthrough

`rtk pip` -- pip / uv

bash
rtk pip list              # Package list (auto-detects uv)
rtk pip outdated          # Outdated packages
rtk pip install <pkg>     # Install

Auto-detects uv when available and uses it instead of pip.


`rtk deps` -- Dependency summary

Purpose: Compact summary of project dependencies.

bash
rtk deps [path]    # Default: current directory

Auto-detects Cargo.toml, package.json, pyproject.toml, go.mod, Gemfile, etc.


`rtk prisma` -- Prisma ORM

Command Description
rtk prisma generate Client generation (removes ASCII art)
rtk prisma migrate dev [--name N] Create and apply a migration
rtk prisma migrate status Migration status
rtk prisma migrate deploy Deploy to production
rtk prisma db-push Schema push

Containers and orchestration

`rtk docker` -- Docker

Command Description
rtk docker ps Compact container list
rtk docker images Compact image list
rtk docker logs <container> Deduplicated logs
rtk docker compose ps Compact Compose services
rtk docker compose logs [service] Deduplicated Compose logs
rtk docker compose build [service] Build summary

Unrecognized subcommands are passed through directly (passthrough).

Before / After:

text
# docker ps (long lines, ~30 tokens/line)         # rtk docker ps (~10 tokens/line)
CONTAINER ID   IMAGE          COMMAND     ...      web  nginx:1.25 Up 2d (healthy)
abc123def456   nginx:1.25     "/dock..."  ...      db   postgres:16 Up 2d (healthy)
789012345678   postgres:16    "docker..."           redis redis:7 Up 1d

`rtk kubectl` -- Kubernetes

Command Description Options
rtk kubectl pods [-n ns] [-A] Compact pod list Namespace or all
rtk kubectl services [-n ns] [-A] Compact service list Namespace or all
rtk kubectl logs [-c container] Deduplicated logs Specific container

Unrecognized subcommands are passed through directly (passthrough).


Data and network

`rtk json` -- JSON structure

Purpose: Shows the structure of a JSON file without the values.

bash
rtk json <file> [--depth N]    # Default: depth 5
rtk json -                     # From stdin

Before / After:

text
# cat package.json (50 lines)               # rtk json package.json (10 lines)
{                                            {
  "name": "my-app",                            name: string
  "version": "1.0.0",                         version: string
  "dependencies": {                            dependencies: { 15 keys }
    "react": "^18.2.0",                        devDependencies: { 8 keys }
    "next": "^14.0.0",                         scripts: { 6 keys }
    ...15 dependencies...                    }
  },
  ...
}

`rtk env` -- Environment variables

bash
rtk env                    # All variables (sensitive ones masked)
rtk env -f AWS             # Filter by name
rtk env --show-all         # Include sensitive values

Sensitive variables (tokens, secrets, passwords) are masked by default: AWS_SECRET_ACCESS_KEY=***.


`rtk log` -- Deduplicated logs

Purpose: Filters and deduplicates log output.

bash
rtk log <file>     # From a file
rtk log            # From stdin (pipe)

Repeated lines are merged: [ERROR] Connection refused (x42).


`rtk curl` -- HTTP with truncation

bash
rtk curl [args...]

Truncates long responses and saves the full output for recovery.


`rtk wget` -- Compact download

bash
rtk wget <url> [args...]
rtk wget -O - <url>           # Output to stdout

Removes progress bars and noise.


`rtk summary` -- Heuristic summary

Purpose: Runs a command and generates a heuristic summary of its output.

bash
rtk summary <command...>
rtk summary --shell fish '<fish command>'

Useful for long-running commands whose output has no dedicated filter.
The command is run directly by default; --shell explicitly turns on
interpretation of a whole command by the chosen shell.


`rtk bypass` -- Passthrough with tracking

Purpose: Runs a command without filtering but records usage for tracking.

bash
rtk bypass <command...>

Useful for debugging: compare raw output with filtered output.


Cloud and databases

`rtk aws` -- AWS CLI

bash
rtk aws <service> [args...]

Forces JSON output and compresses the result. Supports all AWS services (sts, s3, ec2, ecs, rds, cloudformation, etc.).


`rtk psql` -- PostgreSQL

bash
rtk psql [args...]

Removes table borders and compresses the output.


Stacked PRs (Graphite)

`rtk gt` -- Graphite

Command Description
rtk gt log Compact stack log
rtk gt submit Compact submit
rtk gt sync Compact sync
rtk gt restack Compact restack
rtk gt create Compact create
rtk gt branch Compact branch info

Unrecognized subcommands are passed through directly or detected as git passthrough.


Analytics and tracking

Tracking system

RTK records every command execution in a SQLite database:

  • Location: ~/.local/share/rtk/tracking.db (Linux), ~/Library/Application Support/rtk/tracking.db (macOS)
  • Retention: 90 days, automatic
  • Metrics: input/output tokens, bash output reduction percentage, execution time, project

`rtk gain` -- Bash output reduction statistics

bash
rtk gain                        # Global summary
rtk gain -p                     # Filter by current project
rtk gain --graph                # ASCII graph (last 30 days)
rtk gain --history              # Recent command history
rtk gain --daily                # Day-by-day breakdown
rtk gain --weekly               # Weekly breakdown
rtk gain --monthly              # Monthly breakdown
rtk gain --all                  # All breakdowns
rtk gain --quota -t pro         # Estimate of bash output saved against the monthly quota
rtk gain --failures             # Parsing failure log (commands that fell back)
rtk gain --format json          # JSON export (for dashboards)
rtk gain --format csv           # CSV export

Options:

Option Short Description
--project -p Filter by current directory
--graph -g ASCII graph of the last 30 days
--history -H Recent command history
--quota -q Estimate of bash output saved against the monthly quota
--tier -t Subscription tier: pro, 5x, 20x (default: 20x)
--daily -d Daily breakdown
--weekly -w Weekly breakdown
--monthly -m Monthly breakdown
--all -a All breakdowns
--format -f Output format: text, json, csv
--failures -F Show commands that fell back

Example output (illustrative values):

text
$ rtk gain
RTK Token Savings Summary
  Total commands:     1,000
  Total input:        2,000,000 tokens
  Total output:       400,000 tokens
  Total saved:        1,600,000 tokens (80%)
  Avg per command:    1,600 tokens saved

Top commands:
  git status    300x  -80%
  cargo test    150x  -90%
  git diff      100x  -75%

`rtk discover` -- Missed opportunities

Purpose: Analyzes Claude Code history to find commands that rtk could have optimized.

bash
rtk discover                          # Current project, last 30 days
rtk discover --all --since 7          # All projects, last 7 days
rtk discover -p /path/to/project      # Filter by project
rtk discover --limit 20               # Max commands per section
rtk discover --format json            # JSON export

Options:

Option Short Description
--project -p Filter by project path
--limit -l Max commands per section (default: 15)
--all -a Scan all projects
--since -s Last N days (default: 30)
--format -f Format: text, json

`rtk learn` -- Learn from errors

Purpose: Analyzes Claude Code CLI error history to detect recurring corrections.

bash
rtk learn                             # Current project
rtk learn --all --since 7             # All projects
rtk learn --write-rules               # Generate .claude/rules/cli-corrections.md
rtk learn --min-confidence 0.8        # Confidence threshold (default: 0.6)
rtk learn --min-occurrences 3         # Minimum occurrences (default: 1)
rtk learn --format json               # JSON export

`rtk cc-economics` -- Claude Code economics

Purpose: Compares Claude Code spending (via ccusage) with the bash output saved by RTK.

bash
rtk cc-economics                      # Summary
rtk cc-economics --daily              # Daily breakdown
rtk cc-economics --weekly             # Weekly breakdown
rtk cc-economics --monthly            # Monthly breakdown
rtk cc-economics --all                # All breakdowns
rtk cc-economics --format json        # JSON export

`rtk hook-audit` -- Hook metrics

Prerequisite: Requires RTK_HOOK_AUDIT=1 in the environment.

bash
rtk hook-audit                        # Last 7 days (default)
rtk hook-audit --since 30             # Last 30 days
rtk hook-audit --since 0              # Full history

Hook system

How it works

The RTK hook intercepts Bash commands in Claude Code before they run and automatically rewrites them to their RTK equivalent.

Flow:

text
Claude Code "git status"
    |
    v
settings.json -> PreToolUse hook
    |
    v
rtk-rewrite.sh (bash)
    |
    v
rtk rewrite "git status"  ->  "rtk git status"
    |
    v
Claude Code runs "rtk git status"
    |
    v
Filtered output returned to Claude

Key points:

  • Claude never sees the rewrite -- it simply receives optimized output
  • The hook is a lightweight delegator that calls rtk rewrite
  • Commands already prefixed with rtk pass through unchanged
  • Heredocs (<<) are not modified
  • Unrecognized commands pass through unchanged

Installation

bash
rtk init -g                     # Recommended install (hook + RTK.md)
rtk init -g --auto-patch        # Non-interactive (CI/CD)
rtk init -g --hook-only         # Hook only, without RTK.md
rtk init --show                 # Check the installation
rtk init -g --uninstall         # Uninstall

Installed files

File Description
~/.claude/hooks/rtk-rewrite.sh Hook script (delegates to rtk rewrite)
~/.claude/RTK.md Minimal instructions for the LLM
~/.claude/settings.json PreToolUse hook registration

`rtk rewrite` -- Command rewriting

Internal command used by the hooks. The exit code carries the permission decision, taken from Claude Code's permission rules applied to every command in a chain:

  • 0: prints the rewritten command; allow rules cover every command, so the hook may allow it without asking
  • 1: no output, no rewrite (no RTK equivalent, nothing to rewrite, a command excluded in config, a construct RTK does not rewrite such as $(...) or a file redirect, or a deny rule matched inside a shell wrapper's script); the command runs unchanged
  • 2: a deny matched (checked before anything else). With no output, a deny rule matched and the hook defers to the agent's own deny. With output, an rtk pipeline rule denied or routed the command, and the hook blocks it with that message
  • 3: prints the rewritten command; an ask rule matched, or allow rules do not cover every command, including when there are no rules at all; the agent asks for confirmation

An integration therefore reads stdout on 0 and 3 (and on 2, for the block message), and keeps the confirmation prompt on 3. With RTK_REWRITE_HOST=openclaw, an integration that handles approval itself gets 0 in place of every 3 that does not come from an ask rule; an integration that allows without asking on 0 therefore runs rtk rewrite without that variable.

bash
rtk rewrite "git status"           # -> "rtk git status" (exit 3 with no rules)
rtk rewrite "echo hello"           # -> (exit 1, no rewrite)
rtk rewrite "rtk git status"       # -> "rtk git status" (exit 3 with no rules, unchanged)

`rtk verify` -- Integrity check

Checks the integrity of the installed hook with a SHA-256 check.

bash
rtk verify

Automatically rewritten commands

Raw command Rewritten to
git status/diff/log/add/commit/push/pull rtk git ...
gh pr/issue/run rtk gh ...
cargo test/build/clippy/check rtk cargo ...
cat/head/tail <file> rtk read <file>
rg/grep rtk grep
ast-grep run -p rtk ast-grep run -p
ls rtk ls
tree rtk tree
wc rtk wc
jest rtk jest
vitest rtk vitest
tsc rtk tsc
eslint/biome rtk lint
prettier rtk prettier
playwright rtk playwright
prisma rtk prisma
ruff check/format rtk ruff ...
pytest rtk pytest
mypy rtk mypy
pip list/install rtk pip ...
go test/build/vet rtk go ...
golangci-lint rtk golangci-lint
docker ps/images/logs rtk docker ...
kubectl get/logs rtk kubectl ...
curl rtk curl
pnpm list/outdated rtk pnpm ...

Excluding commands

To prevent some commands from being rewritten, add them to config.toml:

toml
[hooks]
exclude_commands = ["curl", "playwright"]

The pattern is anchored at the start of the command: "curl" excludes curl https://... but not curl-config.
Before comparing, RTK strips the wrapper, interpreter or path, so "playwright" covers
playwright test as well as npx playwright test or pnpm exec playwright test, and "pytest" also covers
python3 -m pytest tests/. Arguments are kept: "^ls$" excludes bare ls without
also matching ls -la.


Configuration

Configuration file

Location: ~/.config/rtk/config.toml (Linux) or ~/Library/Application Support/rtk/config.toml (macOS)

Commands:

bash
rtk config                # Show the current configuration
rtk config --create       # Create the file with default values

Full structure

toml
[tracking]
enabled = true              # Enable/disable tracking
history_days = 90           # Retention in days (automatic cleanup)
database_path = "/custom/path/tracking.db"  # Custom path (optional)

[display]
colors = true               # Colored output
emoji = true                # Use emojis
max_width = 120             # Maximum output width

[filters]
ignore_dirs = [".git", "node_modules", "target", "__pycache__", ".venv", "vendor"]
ignore_files = ["*.lock", "*.min.js", "*.min.css"]

[retriever]
mode = "sqlite"             # sqlite (default) | tee (legacy files) | disabled
max_entries = 200           # Max entries in the recall database
retention_days = 30         # Entry retention
# database_path = "/custom/recall.db"  # Custom path (optional)
# A legacy [tee] section is still recognized: mapped to mode = "tee",
# or "disabled" if enabled = false

[telemetry]
enabled = false             # Anonymous telemetry (1 ping/day, requires consent)
# consent_given = true      # Set automatically by `rtk init` or `rtk telemetry enable`
# consent_date = "..."      # Consent date (RFC 3339)

[hooks]
exclude_commands = []       # Commands excluded from automatic rewriting

Environment variables

Variable Description
RTK_RECALL=0 Disable recall saving
RTK_TEE_DIR Override the tee directory ("tee" mode)
RTK_TELEMETRY_DISABLED=1 Disable telemetry
RTK_HOOK_AUDIT=1 Enable hook auditing
SKIP_ENV_VALIDATION=1 Disable env validation (Next.js, etc.)

Recall system

Raw output recovery

When a command fails (or a filter truncates a list), RTK saves the complete raw output in a local SQLite database, addressed by content hash. This lets the LLM retrieve the output without re-running the command.

How it works:

  1. The command fails (exit code != 0) or the output is truncated
  2. RTK stores the raw output (gzip) in ~/.local/share/rtk/recall.db
  3. A hint with the hash is shown: [full output: rtk recall <hash>] or [+N hidden: rtk recall <hash>]
  4. rtk recall <hash> returns the missing part (delta); --full returns the complete output

Commands:

bash
rtk recall <hash>            # Part not shown (delta)
rtk recall <hash> --full     # Complete output
rtk recall <hash> --grep RE  # Filter by regex
rtk recall --list            # List entries
rtk gain --recalls           # Retrieval rate per filter
rtk config recall <mode>     # Change mode without editing the config (sqlite|tee|disabled)

rtk gain --recalls keeps data strictly separated by mode: an exact rate in sqlite mode
(reads go through rtk recall), an approximation (≥) in tee mode (only bash reads
are detectable through the hook, not the Read tool). A high rate indicates a filter
that hides output the agent comes back for.

Configuration:

Setting Default Description
retriever.mode "sqlite" "sqlite", "tee" (legacy files), "disabled"
retriever.max_entries 200 FIFO eviction beyond this
retriever.retention_days 30 Old entries are purged
retriever.max_entry_bytes 10 MB Truncated beyond this (at the last complete line)
Minimum size 500 bytes Failures that are too short are not saved

The "tee" mode keeps the legacy behavior (.log files in ~/.local/share/rtk/tee/, rotation via tee_max_files). A legacy [tee] section in the config is mapped automatically (see above).

Change the mode with rtk config recall <sqlite|tee|disabled> (a targeted edit of
config.toml that preserves comments and migrates legacy [tee] fields); the equivalent
field for manual editing is [retriever] mode.


Telemetry

RTK can send one anonymous ping per day (23-hour interval) for usage statistics. Telemetry is opt-in and disabled by default; it requires explicit consent (GDPR Art. 6, 7). rtk init asks for consent with a [y/N] prompt.

Data sent: device hash (SHA-256 of a random salt), version, OS, architecture, command count over 24h, top commands, bash output reduction percentage, recall counters per filter family (elisions/retrievals -- names taken from a closed allowlist, never a hash, path, argument or content).

Data controller: RTK AI Labs, [email protected]

Managing telemetry:

bash
rtk telemetry status     # Show consent status
rtk telemetry enable     # Give consent (interactive prompt)
rtk telemetry disable    # Withdraw consent
rtk telemetry forget     # Withdraw + delete local data + request server-side erasure

Disable via environment variable:

bash
export RTK_TELEMETRY_DISABLED=1

No personal data, no command content and no file path is transmitted. Server retention: 12 months max. Details: docs/TELEMETRY.md


Commands by category

Category Commands
Files ls, tree, read, find, grep, ast-grep, diff
Git status, log, diff, show, add, commit, push, pull
GitHub pr, issue, run, api
Tests cargo test, vitest, playwright, pytest, go test
Build/Lint cargo build, tsc, eslint, prettier, next, ruff, clippy
Packages pnpm, npm, pip, deps, prisma
Containers docker, kubectl
Data json, env, log, curl, wget
Analytics gain, discover, learn, cc-economics

Coverage

RTK covers the 9 categories above, with automatic passthrough for unrecognized subcommands, so it is always safe to use as a prefix.


Usage TRACKING (`docs/usage/TRACKING.md`)

RTK Tracking

RTK records how much bash output each filtered command removed, so rtk gain can report your savings.

Everything the tracker stores is measured on bash output bytes, converted to estimated tokens (bytes / 4). Bash output is one contributor to input tokens, alongside your prompt, the system prompt and conversation history, and input tokens are in turn only part of the bill, which also counts output tokens. savings_pct is therefore a bash output byte ratio — not a cost figure and not a share of your token bill.

See How RTK Savings Work for the full breakdown.

Where the data lives

Tracking data is stored locally in a SQLite database named history.db:

Platform Default path
Linux ~/.local/share/rtk/history.db
macOS ~/Library/Application Support/rtk/history.db
Windows %LOCALAPPDATA%\rtk\history.db

To use a different location, either set the RTK_DB_PATH environment variable (highest priority) or set it in config.toml:

toml
[tracking]
database_path = "/custom/path/history.db"

What is stored

For each command RTK runs, the database records:

Field Description
timestamp When the command ran (UTC)
original_cmd The command as typed (e.g. git log --oneline -5)
rtk_cmd The RTK command that ran (e.g. rtk git log --oneline -5)
project_path The working directory, used by rtk gain --project
input_tokens Estimated tokens in the raw output (bytes / 4)
output_tokens Estimated tokens after filtering (bytes / 4)
saved_tokens input_tokens - output_tokens
savings_pct saved / input × 100, a bash output byte ratio
exec_time_ms Execution time in milliseconds

The same database also holds filter parse failures and hook rewrite decisions, which feed rtk gain and rtk session.

Commands RTK passes through without filtering are tracked at 0% reduction. They still count toward
commands and the token totals, but they are left out of total_time_ms and avg_time_ms (in the
summary, the daily/weekly/monthly breakdowns, and the JSON/CSV exports): only commands with captured
input (input_tokens > 0) are timed. A passthrough row filtered nothing, and its duration is the
wrapped process's lifetime — for an interactive TUI such as rtk claude, the whole session.

Privacy

  • The tracking database is local only and never leaves your machine.
  • It is separate from telemetry. Telemetry is off by default and sends only anonymous aggregates if you opt in — see Telemetry & Privacy.

Retention

Records older than 90 days are deleted automatically when new records are written.

Viewing your data

bash
rtk gain                  # summary
rtk gain --project        # current project only
rtk gain --daily          # day by day
rtk gain --weekly         # week by week
rtk gain --monthly        # month by month
rtk gain --history        # last 10 commands

See Token Savings Analytics for the full rtk gain surface.

Exporting

bash
rtk gain --all --format json > savings.json
rtk gain --all --format csv  > savings.csv

Example JSON entry for one day (illustrative values):

json
{
  "date": "2026-01-01",
  "commands": 100,
  "input_tokens": 500000,
  "output_tokens": 100000,
  "saved_tokens": 400000,
  "savings_pct": 80.0,
  "total_time_ms": 20000,
  "avg_time_ms": 200
}

Weekly entries carry week_start and week_end instead of date; monthly entries carry month (YYYY-MM).

You can also query the database directly:

bash
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"

Backup

bash
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db

Reset

bash
rtk gain --reset          # asks for confirmation
rtk gain --reset --yes    # skip the prompt (scripts)

Alternatively, delete the database file; it is recreated on the next command:

bash
rm ~/.local/share/rtk/history.db

Troubleshooting

Database locked error

If you see "database is locked" errors:

  • Make sure no other tool holds the database open for writing
  • Check file permissions on history.db
  • As a last resort, delete it: rm ~/.local/share/rtk/history.db && rtk gain

Token counts don't match my provider

Token estimation uses ~4 bytes = 1 token. The percentage is reliable because the same estimator is applied to raw and filtered output, but absolute counts are approximate. For exact counts, run the raw and filtered output through your model's own tokenizer.

See Also


Guide Analytics Discover (`docs/guide/analytics/discover.md`)


title: Discover and Session sidebar: order: 2

Discover and Session

rtk discover — find missed savings

rtk discover analyzes your Claude Code command history to identify commands that ran without RTK filtering, and estimates how much bash output RTK would have removed from them.

bash
rtk discover                    # analyze current project history
rtk discover --all              # all projects
rtk discover --all --since 7    # last 7 days, all projects

Example output (illustrative values):

text
Missed savings analysis (last 7 days)
────────────────────────────────────
Command              Count   Est. lost
cargo test              12     ~48,000 tokens
git log                  8     ~12,000 tokens
pnpm list                3      ~6,000 tokens
────────────────────────────────────
Total missed:           23     ~66,000 tokens

Run `rtk init --global` to capture these automatically.

The ~N tokens figures are estimated bash output bytes divided by 4, not tokens billed by your provider. RTK ships no real tokenizer, and bash output is only one contributor to input tokens. Read them as an order of magnitude of the output volume RTK could have compressed. See How RTK Savings Work.

If commands appear in the missed list after installing RTK, it usually means the hook isn't active for that agent. See Troubleshooting — "AI assistant not using RTK".

rtk session — adoption tracking

rtk session shows RTK adoption across recent Claude Code sessions: how many shell commands ran through RTK vs. raw.

bash
rtk session

Example output (illustrative values):

text
Recent sessions (last 10)
─────────────────────────────────────────────────────
Session                         Total   RTK   Coverage
2026-01-03 10:00  (40 cmds)       40    38      95.0%
2026-01-02 10:00  (40 cmds)       40    40     100.0%
2026-01-01 10:00  (40 cmds)       40    36      90.0%
─────────────────────────────────────────────────────
Average coverage: 95.0%

Low coverage on a session usually means RTK was disabled (RTK_DISABLED=1) or the hook wasn't active for a specific subagent.


Guide Analytics Gain (`docs/guide/analytics/gain.md`)


title: Token Savings Analytics sidebar: order: 1

Token Savings Analytics

rtk gain shows how much bash output RTK has removed across all your commands, with daily, weekly, and monthly breakdowns.

What rtk gain measures is the reduction in bash output bytes, converted to estimated tokens. Bash output is one contributor to input tokens, alongside your prompt, the system prompt and conversation history, and input tokens are in turn only part of the bill, which also counts output tokens. See How RTK Savings Work for the full picture.

Quick reference

bash
# Default summary
rtk gain

# Temporal breakdowns
rtk gain --daily          # all days since tracking started
rtk gain --weekly         # aggregated by week
rtk gain --monthly        # aggregated by month
rtk gain --all            # all breakdowns at once

# Classic flags
rtk gain --graph          # ASCII graph, last 30 days
rtk gain --history        # last 10 commands
rtk gain --quota          # monthly quota savings estimate (default tier: 20x)
rtk gain --quota -t pro   # use pro tier token budget for estimate
rtk gain --recalls        # recall efficiency per filter (calibration)

# Live dashboard
rtk gain                  # auto-launches the dashboard when the RTK Proxy runs
rtk gain --watch          # force the dashboard
rtk gain --text           # force the static view

# Attribution (where the savings came from)
rtk gain --by-method      # per compression method
rtk gain --by-rule        # per pipeline rule
rtk gain --by-layer       # per token layer (read, tool_result, history, ...)
rtk gain --by-pipeline    # per pipeline
rtk gain --pipeline NAME  # one pipeline, sliced by where its savings land
rtk gain --shadow         # what shadow rules would have saved

# Export
rtk gain --all --format json > savings.json
rtk gain --all --format csv  > savings.csv

# Reset
rtk gain --reset          # zero the savings and recall stats (asks first)

Daily breakdown

bash
rtk gain --daily

Example output (illustrative values — yours depend entirely on which commands you run):

text
📅 Daily Breakdown (3 days)
════════════════════════════════════════════════════════════════
Date            Cmds      Input     Output      Saved   Save%
────────────────────────────────────────────────────────────────
2026-01-01       100     500.0K     100.0K     400.0K   80.0%
2026-01-02       100     500.0K     100.0K     400.0K   80.0%
2026-01-03       100     500.0K     100.0K     400.0K   80.0%
────────────────────────────────────────────────────────────────
TOTAL            300       1.5M     300.0K       1.2M   80.0%
  • Cmds: RTK commands executed
  • Input: Estimated tokens from raw command output (bytes / 4)
  • Output: Estimated tokens after filtering (bytes / 4)
  • Saved: Input - Output, in estimated tokens
  • Save%: Saved / Input × 100 — a bash output byte ratio, not a share of your bill

Weekly and monthly breakdowns

bash
rtk gain --weekly
rtk gain --monthly

Same columns as daily, aggregated by Sunday-Saturday week or calendar month.

Export formats

Format Flag Use case
text default Terminal display
json --format json Programmatic analysis, dashboards
csv --format csv Excel, Python/R, Google Sheets

JSON structure:

json
{
  "summary": {
    "total_commands": 300,
    "total_input": 1500000,
    "total_output": 300000,
    "total_saved": 1200000,
    "avg_savings_pct": 80.0
  },
  "daily": [...],
  "weekly": [...],
  "monthly": [...]
}

How token estimation works

rtk gain estimates tokens as bytes / 4; RTK ships no tokenizer. The same estimator is applied to raw and filtered output, so the percentage is reliable; the absolute token counts are approximate and will not match your provider's billing.

text
Input Tokens  = estimate_tokens(raw_command_output)
Output Tokens = estimate_tokens(rtk_filtered_output)
Saved Tokens  = Input - Output
Savings %     = (Saved / Input) × 100

Database

Savings data is stored locally in SQLite:

  • Location: ~/.local/share/rtk/history.db on Linux, ~/Library/Application Support/rtk/history.db on macOS (override with RTK_DB_PATH) — paths below use the Linux location
  • Retention: 90 days (automatic cleanup)
  • Scope: Global across all projects and Claude sessions
bash
# Inspect raw data
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"

# Backup
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db

# Reset
rm ~/.local/share/rtk/history.db    # recreated on next command

Prefer --reset over deleting the file: it zeroes both the token savings and the
recall stats, which live separately, and leaves the database intact.

bash
rtk gain --reset          # asks for confirmation
rtk gain --reset --yes    # skip the prompt (scripts)

Analysis workflows

bash
# Weekly progress: generate a CSV report every Monday
rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv

# Monthly budget review
rtk gain --monthly --format json | jq '.monthly[] |
  {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}'

# Cron: daily JSON snapshot for a dashboard
0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json

Python/pandas:

python
import pandas as pd
import subprocess

result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'],
                       capture_output=True, text=True)
lines = result.stdout.split('\n')
daily_start = lines.index('# Daily Data') + 2
daily_end = lines.index('', daily_start)
daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end])))
daily_df['date'] = pd.to_datetime(daily_df['date'])
daily_df.plot(x='date', y='savings_pct', kind='line')

GitHub Actions (weekly stats):

yaml
on:
  schedule:
    - cron: '0 0 * * 1'
jobs:
  stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
      - run: rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json
      - run: git add stats/ && git commit -m "Weekly rtk stats" && git push

Quota estimate

--quota expresses the estimated tokens saved as a fraction of a monthly subscription budget. Like every other figure in rtk gain, it is derived from the bytes / 4 estimate of bash output, so treat it as an order of magnitude rather than a billing forecast.

bash
rtk gain --quota          # uses 20x tier by default
rtk gain --quota -t pro   # Claude Pro plan budget
rtk gain --quota -t 5x    # 5× usage plan budget
rtk gain --quota -t 20x   # 20× usage plan budget

The tiers (pro, 5x, 20x) correspond to Anthropic Claude API subscription levels, each with a different monthly token allocation. RTK uses those allocations as a denominator to express your savings as a percentage of your budget.

:::tip[Find missed savings]
rtk gain shows what RTK saved. To find commands that ran without RTK and calculate what you lost, see rtk discover.
:::

Live dashboard

--watch replaces the static report with a full-screen live dashboard covering both savings layers at once: the agent hooks trimming command output, and the RTK Proxy compressing billed input tokens.

bash
rtk gain --watch

Auto-launch

A bare rtk gain launches the dashboard automatically once the RTK Proxy is running. All four conditions must hold:

  1. You passed no flag asking for a specific static view — no --daily, --weekly, --monthly, --all, --history, --graph, --quota, and --format left at text.
  2. You did not pass --text.
  3. Standard output is an interactive terminal — piping or redirecting opts out, so scripts and cron jobs are unaffected.
  4. The RTK Proxy is running and reachable.

Without the proxy running, rtk gain behaves exactly as it always has.

--text is the escape hatch — it forces the static view with the proxy up:

bash
rtk gain --text

--watch and --text are mutually exclusive. Asking for --watch without a terminal falls back to the text view with a warning rather than failing.

Attribution

The default report tells you how much was saved. The attribution flags tell you where it came from, which helps you tune a pipeline. Each covers both the hook and proxy sites; proxy figures appear only if you run the RTK Proxy.

bash
rtk gain --by-method      # which compression method did the work
rtk gain --by-rule        # which pipeline rule fired
rtk gain --by-layer       # which token layer (read, tool_result, history, ...)
rtk gain --by-pipeline    # which pipeline

These combine — passing several renders several sections in one report.

One pipeline in detail

--pipeline <NAME> slices a single pipeline by where its savings actually land: which tool outputs the hooks trimmed, and which methods compressed billed input.

bash
rtk gain --pipeline my-pipeline

Shadow rules

Shadow rules are recorded but never emitted — they let you measure a rule's effect before trusting it with real output. --shadow reports what they would have saved:

bash
rtk gain --shadow

A shadow rule showing strong savings is a candidate to promote; one showing nothing is a rule you can delete.

:::note[Full edition only]
Rule, pipeline and shadow attribution describe the compression engine, which the Apache-2.0 edition does not include. See Editions and Licensing.
:::

Recall efficiency

--recalls measures how often your AI assistant goes back for output a filter elided — the signal that a filter's cap is too aggressive for your workflow. Every time a filter stores elided output (an elision) and every time the assistant retrieves it (a recall), RTK counts it per filter:

bash
rtk gain --recalls

Illustrative output:

text
Recall efficiency  (current mode: sqlite)

SQLITE (exact — reads go through rtk recall)
FILTER                   ELISIONS RECALLED   RATE
vitest                         50       20    40%
docker-images                 100        2     2%

TEE (approximate — bash-observed reads only)
FILTER                   ELISIONS RECALLED   RATE
cargo_test                     40        4   ≥10%

How to read it:

  • RATE is the share of elided outputs the assistant went back for. Re-reading the same entry counts once — the rate measures entries consulted, not read commands.
  • A high rate means that filter regularly hides output the assistant needs: every recall is an extra API round-trip you paid for. Consider raising that filter's cap.
  • A low rate means the filter's cap is well calibrated — the elided output was noise.

The two sections are never merged because the data quality differs:

  • SQLITE (exact): reads go through rtk recall <hash>, the only access path, so the count is exact.
  • TEE (approximate): in legacy tee mode, reads are shell commands (tail, cat, grep, …) observed by the rewrite hook before execution. Editor or assistant file-tool reads are invisible, denied commands are not counted, and the per-file dedup window is finite — so the ≥ rate is approximate, not exact. A high rate is still a reliable signal that the cap is too aggressive.

Stats survive entry eviction and retention cleanup: calibration data is kept even after the underlying outputs are purged.

Troubleshooting

No data showing:

bash
ls -lh ~/.local/share/rtk/history.db
sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands"
git status    # run any tracked command to generate data

Incorrect statistics: Token estimation is a heuristic. For precise counts, use tiktoken:

bash
pip install tiktoken
git status > output.txt
python -c "
import tiktoken
enc = tiktoken.get_encoding('cl100k_base')
print(len(enc.encode(open('output.txt').read())), 'actual tokens')
"

Guide Getting Started Configuration (`docs/guide/getting-started/configuration.md`)


title: Configuration sidebar: order: 4

Configuration

Config file location

Platform Path
Linux ~/.config/rtk/config.toml
macOS ~/Library/Application Support/rtk/config.toml
bash
rtk config            # show current configuration
rtk config --create   # create config file with defaults

Full config structure

text
/* Detailed source-code truncated for AI context efficiency. */

For full details on what is collected, opt-out options, and GDPR rights, see Telemetry & Privacy.

Awareness level

rtk init writes a short instructions file for your agent (~/.claude/RTK.md, ~/.gemini/GEMINI.md, …).
awareness.level sets how much it says about RTK. Output is condensed the same way at every level.

Level The agent is told Pick it when
default How to read condensed output. Nothing about RTK. Hook-based agent, RTK stays invisible.
high default + what RTK is and rtk gain, rtk bypass, RTK_DISABLED=1, rtk discover. You want to ask the agent about savings or to bypass RTK.
full high + "prefix every command with rtk". Agent without a hook, or you want the agent to drive RTK itself.
toml
[awareness]
level = "high"

Re-run rtk init -g (or your agent's init command) to rewrite the file.

Agents without a hook (Codex CLI, Cline, Windsurf, Kilo Code, Kimi) always get full,
since the agent must type rtk itself. rtk init prints a note when it does this.

Environment variables

Variable Description
RTK_DISABLED=1 Disable RTK for a single command (RTK_DISABLED=1 git status)
RTK_RECALL=0 Disable the recall store for a single command
RTK_RECALL_DB Override the recall database path
RTK_TEE=0 Legacy alias of RTK_RECALL=0 (still honored)
RTK_TEE_DIR Override the tee directory (tee mode)
RTK_TELEMETRY_DISABLED=1 Disable telemetry
RTK_HOOK_AUDIT=1 Enable hook audit logging
RTK_SUPPRESS_HOOK_WARNING=1 Suppress the missing-hook warning only (not the outdated-hook upgrade prompt); true, yes, on also accepted
RTK_SUPPRESS_HOOK_WARNING=0 Force the missing-hook warning back on, overriding hooks.suppress_hook_warning; false, no, off also accepted
SKIP_ENV_VALIDATION=1 Skip env validation (useful with Next.js)

Recall system

When a command fails — or a filter trims a long list — RTK persists the full output to an embedded database and prints a recall hint:

text
FAILED: 2/15 tests
[full output: rtk recall 36365b69eda6c4f1]

Your AI assistant runs rtk recall <hash> exactly as printed in the hint — that is the whole agent interface. For humans inspecting the store: rtk recall <hash> --full | --from N | --lines N | --grep PAT and rtk recall --list. Storage is byte-faithful (BLOB + lossless gzip); the stored input is the captured command text, as with the previous tee files.

Choosing the recovery mode

The simplest way is the CLI — no file editing needed:

bash
rtk config recall           # show the active mode and its source
rtk config recall sqlite    # hash-addressed sqlite store (default)
rtk config recall tee       # legacy .log files in ~/.local/share/rtk/tee/
rtk config recall disabled  # no recovery storage

Setting a mode rewrites only the relevant keys in config.toml (comments and other sections are preserved), and migrates a legacy [tee] section — its max_files/max_file_size/directory values are carried over. The equivalent config field, if you prefer editing the file directly, is [retriever] mode = "sqlite" | "tee" | "disabled" (see the full structure above).

To see how often your assistant actually goes back for elided output — and which filter caps deserve tuning — see rtk gain --recalls.

Setting Default Description
retriever.mode "sqlite" sqlite (default), tee (legacy files), disabled
retriever.max_entry_bytes 10485760 Per-entry storage cap (10 MiB)
retriever.max_entries 200 FIFO cap on retained entries (0 = no cap)
retriever.retention_days 30 Age eviction in days (0 = off)
retriever.compression true gzip stored blobs (lossless)
retriever.tee_max_files 20 tee mode: how many files are kept before rotation
retriever.tee_max_file_size 1048576 tee mode: per-file cap (1 MiB)
retriever.tee_on_success false tee mode: also archive successful runs — the legacy [tee] mode = "always"
Max file size 1 MB Truncated above this

Excluding commands from auto-rewrite

Prevent specific commands from being rewritten by the hook:

toml
[hooks]
exclude_commands = ["git rebase", "git cherry-pick", "docker exec"]

Patterns match against the full command after stripping env prefixes (VAR=val), so "psql" excludes both psql -h localhost and PGPASSWORD=x psql -h localhost.

Subcommand patterns work too: "git push" excludes git push origin main but not git status.

An entry names a tool RTK has a filter for, and covers the wrapper, interpreter and path spellings
of it. Before matching, RTK peels those off the command and matches what is left, so
"playwright" excludes playwright test, npx playwright test and pnpm exec playwright test
alike; "pytest" also covers python3 -m pytest tests/, and "phpunit" covers
vendor/bin/phpunit and php vendor/bin/phpunit.

Three spellings are not peeled, and still rewrite despite a matching entry:

Entry Command Why
"head", "tail" head -20 f, tail -n 5 f The line-range form takes a fast path that returns before the exclusion is consulted. Without a line range, head f is excluded normally.
"gradlew", "mvn" gradlew.bat build, mvnw.cmd test Path stripping splits on /, so a .bat/.cmd spelling never reduces to the tool name. ./gradlew and gradlew are both excluded.
"golangci-lint" golangci run ./... golangci run is one of the rule's own aliases and is kept whole, so it does not match the golangci-lint entry. Exclude "golangci" as well to cover it.

A tool RTK has no filter of its own for is matched as typed, because RTK only sees the wrapper:
with ["my-tool"], npx my-tool still rewrites to rtk npx my-tool. Exclude "npx" to stop
that.

The arguments are kept when peeling, so an anchored pattern still narrows the way you wrote it:
"^ls$" excludes a bare ls without swallowing ls -la. Matching stays exact — "go" never
excludes golangci-lint, subcommand patterns stay literal ("git push" does not widen to all of
git), and an entry never leaks to a different tool that happens to share an RTK filter: "read"
does not exclude cat, and "eslint" does not exclude biome.

Patterns starting with ^ are treated as regex:

toml
[hooks]
exclude_commands = ["^curl", "^wget", "git rebase"]

Invalid regex patterns fall back to prefix matching.

Or for a single invocation:

bash
RTK_DISABLED=1 git rebase main

Telemetry

Telemetry is off by default and only runs after you opt in — rtk init asks once ([y/N], default no), or run rtk telemetry enable. Once enabled, RTK sends at most one anonymous ping per day. No personal data, no file paths, no command content.

Data sent: device hash, version, OS, architecture, command count/24h, top commands, savings %.

To opt out again:

bash
rtk telemetry disable              # withdraw consent
export RTK_TELEMETRY_DISABLED=1    # block telemetry regardless of config

Custom filters

Add your own filters (or override built-ins) in either location:

  • Project-local — .rtk/filters.toml in your project root (committed with the repo)
  • User-global — ~/.config/rtk/filters.toml (applies to every project)

See src/filters/README.md for the full TOML DSL reference.

Trusting custom filters

Because a filter can rewrite what your AI assistant sees, custom filter files are not applied until you trust them. An untrusted (or edited) filter file is skipped silently on the command path. You review and manage trust with explicit commands:

bash
rtk trust      # shows each filter and asks to confirm (--yes to skip the prompt)
rtk untrust    # revokes trust

rtk init also detects existing filters and lets you enable them — interactively, or non-interactively with --trust-filters / --no-trust-filters. Trust is tied to the file's contents (SHA-256), so editing a trusted file requires re-running rtk trust.

Upgrading: earlier versions applied ~/.config/rtk/filters.toml without trust. After upgrading, the user-global file is gated like project filters — if you already relied on a global filter, run rtk trust once to re-enable it.


Guide Getting Started Installation (`docs/guide/getting-started/installation.md`)


title: Installation sidebar: order: 1

Installation

Name collision warning

Two unrelated projects share the name rtk. Make sure you install the right one:

  • Rust Token Killer (rtk-ai/rtk) — this project, a token-saving CLI proxy
  • Rust Type Kit (reachingforthejack/rtk) — a different tool for generating Rust types

The easiest way to verify you have the correct one: run rtk gain. It should display the savings dashboard. If it returns "command not found", you either have the wrong package or RTK is not installed.

Check before installing

bash
rtk --version   # should print: rtk x.y.z
rtk gain        # should show the savings dashboard

If both commands work, RTK is already installed. Skip to Project initialization.

Quick install (Linux and macOS)

bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh

Installs to ~/.local/bin. Add it to your PATH if it is not there already:

bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc   # or ~/.zshrc

Homebrew (macOS and Linux)

bash
brew install rtk-ai/tap/rtk

winget (Windows)

powershell
winget install rtk-ai.rtk

Cargo

:::caution[Name collision risk]
cargo install rtk may install Rust Type Kit instead of Rust Token Killer — two unrelated projects share the same crate name. Use the explicit Git URL to guarantee the correct package:
:::

bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk

Pre-built binaries (Windows, Linux, macOS)

Download from GitHub releases:

  • macOS: rtk-x86_64-apple-darwin.tar.gz / rtk-aarch64-apple-darwin.tar.gz
  • Linux: rtk-x86_64-unknown-linux-musl.tar.gz / rtk-aarch64-unknown-linux-gnu.tar.gz
  • Windows: rtk-x86_64-pc-windows-msvc.zip

Windows users: Extract the zip and place rtk.exe in a directory on your PATH. Run RTK from Command Prompt, PowerShell, or Windows Terminal — do not double-click the .exe (it prints usage and exits immediately). See Windows below — the hook system works natively, no WSL required.

Apache-2.0 edition

The same rtk binary built without the BUSL-1.1 compression engine — the hook rewrite system and every command filter, under Apache-2.0 alone. Pick this if your policy does not allow a source-available licence in the build.

bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | RTK_EDITION=oss sh
brew install rtk-ai/tap/rtk-oss
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk --no-default-features

Release assets are named rtk-oss-<target>, with rtk-oss Debian and RPM packages alongside. The binary is still called rtk, so hooks and scripts are unchanged — but the two editions conflict and cannot be installed together.

See Editions and Licensing for what the edition omits and where the licence boundary falls.

Windows

RTK works fully on native Windows. Since v0.37.2 the auto-rewrite hook runs as a native binary command (rtk hook claude) — no Unix shell, bash, or jq required — so commands are rewritten transparently in Command Prompt, PowerShell, and Windows Terminal, exactly as on Linux and macOS.

Native Windows (manual install)

Prefer winget if you can — it handles PATH for you.

powershell
# 1. Download and extract rtk-x86_64-pc-windows-msvc.zip from releases
# 2. Add rtk.exe to your PATH (e.g. C:\Users\<you>\.local\bin)
# 3. Initialize - installs the native binary hook
rtk init -g

Upgrading from an older install? If you set RTK up before v0.37.2 you may still have the legacy rtk-rewrite.sh shell hook, which does need a Unix shell. Re-run rtk init -g to migrate to the native binary hook.

Prerequisites: some filters shell out to ripgrep (rg). Install it and keep it on your PATH to avoid Binary 'rg' not found on PATH warnings:

powershell
winget install BurntSushi.ripgrep.MSVC

WSL

WSL also works and behaves exactly like Linux:

bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
rtk init -g
Feature Native Windows WSL
Filters (cargo, git, …) Full Full
Auto-rewrite hook Yes (native binary) Yes
rtk init -g Hook mode Hook mode
rtk gain / analytics Full Full

Verify installation

bash
rtk --version   # rtk x.y.z
rtk gain        # savings dashboard

If rtk gain fails but rtk --version succeeds, you installed Rust Type Kit by mistake. Uninstall it first:

bash
cargo uninstall rtk

Then reinstall using one of the methods above.

Project initialization

For a single project, rtk init adds the RTK block to ./CLAUDE.md and a .rtk/filters.toml template, without installing a hook:

bash
rtk init

To install the Claude Code hook (registered in ~/.claude/settings.json, applies to every project):

bash
rtk init --global

Uninstall

bash
rtk init -g --uninstall    # remove hook, RTK.md, and settings.json entry
cargo uninstall rtk         # remove binary (if installed via Cargo)
brew uninstall rtk          # remove binary (if installed via Homebrew)
winget uninstall rtk-ai.rtk # remove binary (if installed via winget)

Guide Getting Started Quick Start (`docs/guide/getting-started/quick-start.md`)


title: Quick Start sidebar: order: 2

Quick Start

This guide walks you through your first RTK commands after installation.

Prerequisites

RTK is installed and verified:

bash
rtk --version   # rtk x.y.z
rtk gain        # shows token savings dashboard

If not, see Installation.

Step 1: Initialize for your AI assistant

bash
# For Claude Code (global — applies to all projects)
rtk init --global

# For a single project only (CLAUDE.md instructions, no hook)
cd /your/project && rtk init

rtk init --global installs the hook that automatically rewrites commands. Restart your AI assistant after this step.

Using something other than Claude Code? Every agent has its own flag:

bash
rtk init -g --gemini            # Gemini CLI
rtk init -g --codex             # Codex (OpenAI)
rtk init -g --agent cursor      # Cursor
rtk init -g --agent windsurf    # Windsurf
rtk init --agent cline          # Cline / Roo Code

See Supported agents for the full list, including per-agent uninstall and the project-local vs global distinction.

Preview without writing: `--dry-run`

To see exactly what init would change before it touches anything, add --dry-run:

bash
rtk init --global --dry-run

Every would-be file create/update/patch is printed with a [dry-run] would ... prefix, then a [dry-run] Nothing written. footer. Nothing on disk is modified, no settings.json is patched, and the telemetry consent prompt is skipped. Combine with -v to also print the full content RTK would write:

bash
rtk init --global --dry-run -v

--dry-run works for every init flavour (--agent cursor, --gemini, --codex, --copilot, --uninstall, ...). It cannot be combined with --show.

Step 2: Use your tools normally

Once the hook is installed, nothing changes in how you work. Your AI assistant runs commands as usual — the hook intercepts them transparently and rewrites them before execution.

For example, when Claude Code runs cargo test, the hook rewrites it to rtk cargo test before it executes. The LLM receives filtered output with only the failures — not 500 lines of passing tests. You never see or type rtk.

RTK covers all major ecosystems — Git, Cargo/Rust, JavaScript, Python, Go, Ruby, .NET, Docker/Kubernetes, and more. See What RTK Optimizes for the full list.

Step 3: Check your savings

After a few commands, see how much was saved:

bash
rtk gain

Illustrative output:

text
Total commands : 10
Input tokens   : 50,000
Output tokens  : 5,000
Saved          : 45,000  (90.0%)

:::caution[The hook only fires on Bash tool calls]
Built-in agent tools like Read, Grep and Glob do not pass through the Bash hook, so they are not auto-rewritten. To get RTK's compact output for those workflows, either use shell commands (cat/head/tail, rg/grep, find) or call rtk read, rtk grep and rtk find directly.

The full edition can additionally compress an agent's own Read and MCP tool output through the PostToolUse hook — see Pipelines.
:::

Simple quoted shell wrappers are rewritten too, without changing the shell they name:

bash
bash -c "head foo && grep -R bar ."
# → bash -c "rtk read foo --head-lines 10 && rtk grep -R bar ."

This covers exact sh -c, bash -c, zsh -c and fish -c wrappers around a quoted script. A fish -c script is read as fish, so fish's own syntax ((cmd) substitution, and/or/end) is left alone rather than rewritten under POSIX rules:

bash
fish -c 'git status; cargo test'      # → fish -c 'rtk git status; rtk cargo test'
fish -c 'git status; and cargo test'  # unchanged

Shell expansion inside an outer double quote, extra shell options, redirects to files and nested wrappers pass through unchanged.

Step 4: Unsupported commands

Commands RTK doesn't recognize run through passthrough — output is unchanged, usage is tracked:

bash
rtk bypass make install

Next steps


Guide Getting Started Supported Agents (`docs/guide/getting-started/supported-agents.md`)


title: Supported Agents sidebar: order: 3

Supported Agents

RTK supports all major AI coding agents across 3 integration tiers.

How it works

Each agent integration intercepts CLI commands before execution and rewrites them to their RTK equivalent. The agent runs rtk cargo test instead of cargo test, sees filtered output, and reads up to 90% fewer bash output bytes — without any change to your workflow.

All rewrite logic lives in the RTK binary (rtk rewrite). Agent hooks are thin delegates that parse the agent-specific JSON format and call rtk rewrite for the actual decision.

text
Agent runs "cargo test"
  -> Hook intercepts (PreToolUse / plugin event)
  -> Calls rtk rewrite "cargo test"
  -> Returns "rtk cargo test"
  -> Agent executes filtered command
  -> LLM reads the condensed output

Supported agents

Agent Integration tier Can rewrite transparently?
Claude Code Rust binary (PreToolUse, rtk hook claude) Yes
Trae Rust binary (PreToolUse, matcher RunCommand) Yes
VS Code Copilot Chat Rust binary (PreToolUse, rtk hook copilot) Yes
GitHub Copilot CLI Rust binary (PreToolUse, rtk hook copilot) Yes
Cursor Rust binary (preToolUse, rtk hook cursor) Yes
Gemini CLI Rust binary (BeforeTool) Yes
OpenCode TypeScript plugin (execute.before in 2.0, tool.execute.before in 1.x >= 1.18.29) Yes
OpenClaw TypeScript plugin (before_tool_call) Yes
Pi TypeScript extension (tool_call event) Yes
Oh My Pi (OMP) TypeScript extension (tool_call event, shared with Pi) Yes
Hermes Python plugin (terminal command mutation) Yes
Factory Droid Rust binary (PreToolUse, matcher Execute) Yes
Cline / Roo Code Rules file (prompt-level) N/A
Windsurf Rules file (prompt-level) N/A
Codex CLI Rust binary (PreToolUse) Yes
Kilo Code Rules file (prompt-level) N/A
Google Antigravity Rust binary (PreToolUse plugin) Yes
Mistral Vibe Rust binary (pre_tool) Yes

Agents that rewrite transparently receive the awareness file selected by awareness.level in
config.toml (default says nothing about RTK). Rules-file agents cannot rewrite, so the agent
must prefix rtk itself; they always receive the full awareness file and rtk init says so.
See Configuration.

:::note[How much of a pipeline reaches your agent?]
Command rewriting works on almost every agent here. Compressing tool output
after the fact depends on whether the agent lets a hook replace it — eight do.
See Agent Coverage for the per-agent table.
:::

Installation by agent

Claude Code

bash
rtk init --global    # installs hook + patches settings.json

Restart Claude Code. Verify:

bash
rtk init --show    # shows hook status

Trae

bash
rtk init --agent trae             # project-scoped (.trae/hooks.json)
rtk init --global --agent trae    # user-scoped (~/.trae/hooks.json)

Global installation also updates ~/.trae-cn/hooks.json when the .trae-cn directory already exists. Both modes install the native rtk hook trae command as a PreToolUse hook for RunCommand.

Configuration files and hook input may include a UTF-8 BOM. Existing rtk.exe hook trae registrations are also recognized during install and uninstall.

If a global install fails while writing a target, the error lists any targets already updated. After fixing the reported filesystem error, rerun the install; completed targets will not receive duplicate hooks.

The hook returns hookSpecificOutput.updatedInput, preserving fields such as description and timeout while replacing only command. It deliberately omits permissionDecision, leaving command approval to Trae. Commands containing command substitution, process substitution, heredocs, or file-target redirects are left unchanged so Trae evaluates the original command natively.

Uninstall:

bash
rtk init --uninstall --agent trae
rtk init --uninstall --global --agent trae

Uninstall removes only RTK-managed hook entries; unrelated Trae hooks and configuration are preserved.

Cursor

bash
rtk init --global --agent cursor

Restart Cursor. The hook uses preToolUse with Cursor's updated_input format.

GitHub Copilot (VS Code Chat + CLI)

bash
rtk init --copilot            # project-scoped (.github/hooks/)
rtk init --global --copilot   # user-scoped (~/.copilot/hooks/, respects $COPILOT_HOME)

Project-scoped writes .github/hooks/rtk-rewrite.json — a single PreToolUse entry shared by both hosts, each getting transparent rewrite via updatedInput — plus the RTK block in .github/copilot-instructions.md. User-scoped writes the same hook config to ~/.copilot/hooks/rtk-rewrite.json and the RTK block to ~/.copilot/copilot-instructions.md (both respect $COPILOT_HOME if set).

Earlier rtk versions also registered a second, camelCase preToolUse entry, which made Copilot CLI run the hook twice per tool call. Re-run rtk init --copilot (or --global --copilot) to upgrade an existing install to the single-hook config.

Uninstall:

bash
rtk init --uninstall --copilot
rtk init --uninstall --global --copilot

Removes only RTK's hook file (and, for project, the RTK block in copilot-instructions.md). Other files in .github/hooks/ or ~/.copilot/hooks/ and your own instruction content are untouched.

Gemini CLI

bash
rtk init --global --gemini

OpenCode

bash
rtk init --global --opencode

Installs the Claude Code setup and creates ~/.config/opencode/plugins/rtk.ts. The plugin registers both OpenCode 2.0 (execute.before) and OpenCode 1.x (tool.execute.before) hooks, in one default export carrying both entrypoints. That shape is supported from OpenCode 1.18.29, the floor OpenCode documents for it; with an older OpenCode, when it can read opencode --version, rtk init writes nothing and says so.

Pi

bash
# Project-local (default)
rtk init --agent pi

# Global — all projects
rtk init --agent pi --global

Creates .pi/extensions/rtk.ts (local) or ~/.pi/agent/extensions/rtk.ts (global). Pi auto-discovers extensions from both paths on startup. The global path follows PI_CODING_AGENT_DIR when set; OMP uses that same variable. When the paths alias, RTK records the agent(s) it installed in an adjacent hidden .rtk-agents state file so shared-file warnings are based on ownership rather than path equality alone.

Installation updates only the current or a known historical RTK extension. If the managed path contains modified or unrelated content, RTK asks before overwriting it; use --auto-patch to approve without prompting or --no-patch to leave it unchanged. A declined protected update, including --no-patch, exits nonzero so automation can detect that no install occurred. --dry-run reports the prompt without changing files. LF and CRLF line endings are treated as the same stock extension.

Uninstall:

bash
rtk init --uninstall --agent pi
rtk init --uninstall --agent pi --global

Removes only the current or known historical stock Pi extension. If the file contains modified RTK content, a normal uninstall aborts without deleting it, while --dry-run previews that refusal; unreadable content is left in place and causes a normal uninstall to exit nonzero, while --dry-run reports it and succeeds. Unrelated content is left in place. LF and CRLF stock files are both recognized.

Oh My Pi (OMP)

bash
# Project-local (default)
rtk init --agent omp

# Global — all projects
rtk init --agent omp --global

Creates .omp/extensions/rtk.ts (local) or ~/.omp/agent/extensions/rtk.ts (global). OMP loads the same extension file as Pi through its legacy-pi-compat layer, so both agents share rtk.ts. The global path follows PI_CODING_AGENT_DIR, which OMP also honors for its agent directory.

When the Pi and OMP targets in either project or global scope resolve to one file, RTK records whether Pi, OMP, or both agents were installed for that alias in an adjacent hidden .rtk-agents state file. A valid sidecar is authoritative. Missing or unreadable state is treated as uncertain: RTK warns and proceeds without using a heuristic to claim sole ownership. Uninstalling a definitively shared project or global file asks before removing it, so confirm that neither agent should use it first (or pass --auto-patch to approve). An uncertain legacy or corrupt-state uninstall warns and proceeds; a declined definitive shared uninstall, including --no-patch, exits nonzero. --dry-run remains a successful preview. RTK currently targets OMP's default profile and .omp project directory; named OMP profiles and custom PI_CONFIG_DIR locations are not auto-detected.

Installation updates only the current or a known historical RTK extension. If the managed path contains modified or unrelated content, RTK asks before overwriting it; use --auto-patch to approve without prompting or --no-patch to leave it unchanged. A declined protected update, including --no-patch, exits nonzero so automation can detect that no install occurred. --dry-run reports the prompt without changing files. LF and CRLF line endings are treated as the same stock extension.

Uninstall:

bash
rtk init --uninstall --agent omp
rtk init --uninstall --agent omp --global

Removes only the current or known historical stock OMP extension. If the file has been modified after install, a normal uninstall aborts with a message instead of removing it, while --dry-run previews that refusal; unreadable content is left in place and causes a normal uninstall to exit nonzero, while --dry-run reports it and succeeds. Unrelated content is left in place. When Pi and OMP paths in either scope resolve to the same file and a valid sidecar records both agents, uninstall asks before removing the shared file; use --auto-patch to approve or --no-patch to keep it. Missing or unreadable ownership state warns and proceeds without definitive shared-file protection. A declined definitive shared uninstall exits nonzero so scripts can detect that the file remains.

OpenClaw

bash
openclaw plugins install ./openclaw

Plugin in the openclaw/ directory. Uses the before_tool_call hook, delegates to rtk rewrite.

Permissions. RTK keeps the deny gate; OpenClaw owns approval. The plugin runs rtk rewrite with RTK_REWRITE_HOST=openclaw, which tells RTK that this host applies its own exec policy (tools.exec.mode, security, ask) to whatever the hook returns. RTK therefore does not prompt for a command that matched no rule, instead of raising a second approval derived from Claude Code's settings.json on a runtime that never opted into it (#3908).

A command matching a permissions.deny rule in those same Claude Code settings files is still refused, and the plugin blocks the tool call — naming the host only relaxes a default ask. As in Claude Code, a deny or ask rule matches the command as written and the command behind any leading assignment or wrapper (HUSKY=0, timeout 30, nohup, command, ...), each layer in turn, but not every way of invoking the program (Bash(git push *) does not stop git -C . push or env -i git push), so a deny rule is not a security boundary. Allow rules match the command as written. A command matching a permissions.ask rule you wrote still prompts when RTK rewrites it, because RTK keeps returning exit 3 for it. Commands containing a command substitution or a redirect to a file are never rewritten, on any host.

The exec tool's own checks see the rewritten command. OpenClaw carries hook adjustments forward into the parameters passed to the exec tool, so tools.exec.mode, tools.exec.security, tools.exec.ask and the exec-approvals allowlist are all matched against rtk git push, not git push; write those rules against the rtk form. That was already true before the permission change. A trusted tool policy (api.registerTrustedToolPolicy(...)) is the exception: OpenClaw runs trusted policies before ordinary before_tool_call hooks, so one of those still sees the original command.

No minimum rtk version: an rtk that predates RTK_REWRITE_HOST ignores it and keeps its previous behaviour, which is a prompt rather than a missing gate — an older rtk prompts for more commands, since it cannot collapse the default ask.

Hermes

bash
rtk init --agent hermes

Creates ~/.hermes/plugins/rtk-rewrite/ and enables it through plugins.enabled in the Hermes config. Hermes loads Python plugins, so the plugin entrypoint is Python, but it is only a thin adapter. It mutates the Hermes terminal tool command before execution and delegates all rewrite decisions to rtk rewrite.

The plugin fails open. If rtk is missing at load time, the hook is not registered. If rtk rewrite errors, the tool is not terminal, the payload has no string command, or the plugin raises an exception, Hermes runs the original command unchanged. The same rtk rewrite limitations apply: already-prefixed rtk commands, compound shell commands, heredocs, and commands without filters are not rewritten.

Factory Droid

bash
rtk init -g --agent droid    # user-scoped (~/.factory/hooks.json)
rtk init --agent droid       # project-scoped (.factory/hooks.json, commit to share)

Installs a PreToolUse hook (matcher Execute) into Droid's canonical hooks.json — falling back to the hooks key of settings.json only when that file already carries live PreToolUse hooks. Respects $FACTORY_HOME_OVERRIDE.

RTK honors Droid's own permission lists, never another agent's settings. Commands matching an explicit commandDenylist or commandBlocklist entry — read from all four settings scopes (~/.factory/settings.json, ~/.factory/settings.local.json, .factory/settings.json, .factory/settings.local.json) — are left untouched so Droid's native confirmation or block fires on the original command. Every other command is rewritten via updatedInput with no permission decision: Droid's native flow (allowlist, autonomy level, other hooks) decides on the rewritten command. To auto-run rewritten read-only commands, add rtk-prefixed entries (e.g. rtk git status) to your commandAllowlist.

Uninstall:

bash
rtk init --uninstall -g --agent droid
rtk init --uninstall --agent droid

Removes only RTK's hook entry; other hooks and settings are untouched.

Cline / Roo Code

bash
rtk init --agent cline    # creates .clinerules in current project

Cline reads .clinerules as custom instructions. RTK adds guidance telling Cline to prefer rtk <cmd> over raw commands.

Windsurf

bash
rtk init --global --agent windsurf    # creates .windsurfrules in current project

Codex CLI

bash
rtk init --codex           # project-scoped (.codex/hooks.json + AGENTS.md)
rtk init --global --codex  # user-global ($CODEX_HOME or ~/.codex/)
rtk init --codex --uninstall           # remove project-scoped integration
rtk init --global --codex --uninstall  # remove user-global integration

Restart Codex after installation. Project-scoped hooks must be trusted when Codex prompts. The native rtk hook codex processor rewrites supported Bash commands through PreToolUse.updatedInput; Codex then applies its normal approval and sandbox checks to the rewritten command.

Project-scoped install writes RTK.md to the project root, a name RTK does not own there, so it marks the files it wrote. An RTK.md is RTK's when it carries that marker, when it opens with the heading RTK wrote before the marker existed, or when it is byte-for-byte one of the payloads RTK shipped in between: install replaces it and uninstall removes it. Any other RTK.md is yours — install moves it to RTK.md.bak (numbered if that name is taken) and says so, and uninstall keeps it and tells you where it is. In global scope RTK.md lives in $CODEX_HOME and is always RTK's.

Project-scoped install also refuses, without writing anything, when .codex/hooks.json or the hooks.json.bak it would write beside it resolves outside the project through a symlink, since registering a hook there would run commands from a directory you never named. Uninstall leaves such a hook registered rather than reaching outside for it, and says so. Use the global scope, with $CODEX_HOME set if you want a different directory, to configure Codex outside the project.

AGENTS.md and RTK.md are not restricted this way, and git stores symlinks, so a repository you clone can ship either as a link pointing outside the clone.

A symlinked AGENTS.md is followed and left in place: install appends its @RTK.md line to whatever the link names, wherever that is, and the file it rewrites ends up owned by you and readable only by you. That line is inert text, unlike .codex/hooks.json, which registers a hook that runs shell commands.

A symlinked RTK.md depends on what it points at. If the target is not one RTK wrote, the link itself is moved to RTK.md.bak and a fresh file takes its place, so nothing outside is touched. If the target is one RTK wrote — a shared or global RTK.md, say — install follows the link and rewrites that file in full.

If you install RTK into repositories you have not read, check what AGENTS.md and RTK.md are first.

Kilo Code

bash
rtk init --agent kilocode    # creates .kilocode/rules/rtk-rules.md in current project

Kilo Code reads .kilocode/rules/ as custom instructions. RTK adds guidance telling Kilo Code to prefer rtk <cmd> over raw commands.

Google Antigravity (CLI, IDE & 2.0)

bash
rtk init --agent antigravity          # workspace-scoped (<repo>/.agents/plugins/rtk/)
rtk init -g --agent antigravity       # machine-scoped (~/.gemini/config/plugins/rtk/)

Installs an Antigravity plugin bundle: hooks.json maps PreToolUse on run_command to the native rtk hook antigravity binary, and rules/AGENTS.md carries the awareness file selected by awareness.level. Before any command executes, RTK rewrites the tool call arguments in place using overwrite.CommandLine.

Antigravity checks permissions after lifecycle hooks rewrite a command. If you enforce command allowlists, ensure permitted commands include rtk (e.g. command(rtk git status) or command(rtk *)).

Uninstall:

bash
rtk init --agent antigravity --uninstall       # workspace
rtk init -g --agent antigravity --uninstall    # global

Mistral Vibe

bash
rtk init -g --agent vibe                # user-scoped (~/.vibe/hooks.toml)
rtk init -g --agent vibe --hook-only    # skip the ~/.vibe/prompts/rtk.md prompt file

Installs a pre_tool hook entry (match = "bash", command = "rtk hook vibe", strict = false) into ~/.vibe/hooks.toml, following the contract at docs.mistral.ai/vibe/code/cli/hooks. Vibe invokes the native rtk hook vibe binary before every bash tool call; RTK reads Vibe's stdin JSON payload and emits {"hook_specific_output": {"tool_input": {"command": "rtk ..."}}} to rewrite the command in place. The Vibe UI surfaces [rtk-rewrite] rtk: rewrote to \…`via RTK'ssystem_message` field so the rewrite is visible.

Unlike Droid, Vibe does not expose a denylist / allowlist surface in hooks.toml for RTK to honor. RTK therefore rewrites every bash command it knows how to compress and defers to Vibe's own permission prompt on the rewritten command; commands RTK doesn't handle pass through unchanged. strict = false ensures a hook crash degrades to a warning rather than blocking the tool call.

Alongside the hook, RTK drops a system prompt at ~/.vibe/prompts/rtk.md describing the RTK conventions to Vibe as a fallback. Use --hook-only to skip it.

Install is global-only (Vibe's hook registry is user-scoped). Re-running the installer is a no-op; the RTK entry is detected by its name = "rtk-rewrite" field and never duplicated.

Uninstall:

bash
rtk init -g --agent vibe --uninstall

Strips only RTK's [[hooks]] block and the ~/.vibe/prompts/rtk.md file. Any other user-declared hooks in hooks.toml are preserved byte-for-byte. hooks.toml is removed only when the RTK entry was the sole content.

Integration tiers explained

Tier Mechanism How rewrites work
Full hook Rust binary command (or a thin wrapper around one), intercepts via agent API Transparent — agent never sees the raw command
Plugin TypeScript, JavaScript, or Python in agent's plugin system Transparent, in-place mutation when the agent allows it
Rules file Prompt-level instructions Guidance only — agent is told to prefer rtk <cmd>

Rules file integrations (Cline, Windsurf, Kilo Code) rely on the model following instructions. Full hook integrations (Claude Code, Trae, Cursor, Gemini, Codex, Factory Droid, Antigravity) apply rewrites before execution whenever RTK supports and can safely attest the command. Plugin integrations (OpenCode, Pi, Hermes) use in-place mutation via the agent's extension or plugin API.

Windows support

Since v0.37.2 the hooks for Claude Code, Cursor, Copilot, Codex, Trae, Factory Droid and Mistral Vibe are native binary commands (rtk hook <agent>), so auto-rewrite works on native Windows — no Unix shell, bash or jq required. rtk init -g installs the native hook on Windows exactly as on Linux and macOS, and adds it to Claude Code's settings.json once you answer y to the prompt (or pass --auto-patch).

The Gemini CLI hook is a small bash wrapper around rtk hook gemini, so it needs a bash available; WSL behaves identically to Linux.

Setups created before v0.37.2 used CLAUDE.md injection with no hook, or the legacy rtk-rewrite.sh shell hook. Re-run rtk init -g to migrate them to the native binary hook.

Graceful degradation

Hooks never block command execution. If RTK is missing, the hook exits cleanly and the raw command runs unchanged:

  • RTK binary not found: warning to stderr, exit 0
  • Invalid JSON input: pass through unchanged
  • RTK version too old: warning to stderr, exit 0
  • Filter logic error: fallback to raw command output

Override: disable RTK for one command

bash
RTK_DISABLED=1 git status    # runs raw git status, no rewrite

Or exclude commands permanently in ~/.config/rtk/config.toml:

toml
[hooks]
exclude_commands = ["git rebase", "git cherry-pick"]

Guide Pipelines Commands (`docs/guide/pipelines/commands.md`)


title: Pipeline Commands sidebar: order: 8

Pipeline Commands

rtk pipeline is how you inspect and debug the compression engine: what pipeline is active, what it does to a given input, and why a particular rule fired. See Pipelines for the concepts and Methods for the transforms themselves.

:::note[Full edition only]
rtk pipeline, rtk methods and rtk tasks are part of the compression engine and are absent from the Apache-2.0 edition. See Editions and Licensing.
:::

Quick reference

bash
rtk pipeline show              # print the resolved pipeline
rtk pipeline validate          # validate every pipeline file, non-zero on error
rtk pipeline test              # parse, resolve and validate every loaded file
rtk pipeline status            # last recorded pipeline load problem, if any
rtk pipeline explain           # what fires for a given agent/tool, and why
rtk pipeline bench <file>      # before/after tokens over a local file
rtk pipeline share             # paste-ready markdown card of a pipeline
rtk pipeline share publish     # publish it: GitHub repo (recommended) or gist
rtk pipeline add <source>      # adopt a shared pipeline: validate, install, report what it needs
rtk pipeline search [query]    # find shared pipelines on GitHub (topic rtk-pipeline)
rtk pipeline promote <name>    # move a shadow install live (--discard to drop it)
rtk pipeline eval <name>       # shadow-evaluate an installed pipeline on your live sessions
rtk pipeline log tail          # the opt-in decision log

Inspecting what is active

bash
rtk pipeline show

Prints the pipeline that actually resolved for this directory. To print a built-in verbatim instead, name it:

bash
rtk pipeline show rtk:default
rtk pipeline show rtk:posthook

Validating your own pipelines

validate exits non-zero on errors, which makes it usable as a pre-commit or CI check:

bash
rtk pipeline validate

test is the broader check — it parses, resolves and validates every pipeline file RTK loads, not just the active one. When RTK silently fell back because a file failed to load, status is where the reason is recorded:

bash
rtk pipeline test
rtk pipeline status

Validation never writes anything into the agent's session. If a file fails to load while the agent is working, RTK drops that file only — your other pipeline files, their deny rules included, stay active — records the problem silently, and shows it the next time you run rtk gain, rtk pipeline status or validate. Every code is listed in Errors and Warnings.

Explaining a decision

explain answers "why did this get compressed the way it did?" for a hypothetical input:

bash
rtk pipeline explain --agent claude-code --tool Bash --command "git log -20"
rtk pipeline explain --tool Read --file src/main.rs
rtk pipeline explain --tool Read --file big.json --ranged
Flag Meaning
--agent Agent to explain for (claude-code, cursor, cline, …)
--tool Tool name (Bash, Read, Grep, WebFetch, …). Defaults to Bash, or Read with --file
--file File path, for the before_tool.read conditions (file_lines, extension, …)
--ranged With --file: simulate a ranged read rather than a whole-file read
--format Content format (term, json, web, matches, lockfile)
--command Command line, for the command_* conditions
--last Explain the most recent real decision from the log instead of a hypothetical

--file reads the same facts the hook reads, so a dry run cannot disagree with the live decision. Pipe output in to supply the after-tool size conditions (bytes, lines, tokens); without it a rule gated on output size cannot match:

bash
rtk pipeline explain --tool Grep < output.txt

It reports the position, the rule that fired and why, the methods applied, tokens before and after, the recovery hint — and, for a rule that cannot run on your agent, which capability is missing.

--last is usually the fastest route when something surprising just happened — it needs the decision log enabled.

Shadow mode

toml
[[hook.after_tool.shell]]
name   = "gentle-term"
when   = { is_rtk_command = false }
use    = ["ansi", "dedup"]
shadow = true

The rule runs, its decision and would-be savings are logged, nothing is emitted. rtk gain --shadow shows what it would have saved; turn shadow off when the numbers convince you. Transforms only, and hook rules only: a shadow proxy rule is skipped (W018). To try a whole shared pipeline the same way, see shadow installs.

Measuring a pipeline

bench runs the resolved chain over a local file and reports before/after tokens:

bash
rtk pipeline bench my-output.txt

Use it to check whether a pipeline change helps on your own data.

The decision log

Off by default. When enabled it records decisions and commands, never content:

bash
rtk pipeline log on
rtk pipeline log tail                    # most recent 20 entries
rtk pipeline log tail --limit 100
rtk pipeline log show --rule my-rule     # filter by rule name
rtk pipeline log show --limit 200
rtk pipeline log off

Each entry records timestamp, session, position, tool category, the command as the agent issued it, the rule that fired, action or methods, tokens before and after, shadow flag and recovery hint — never a tool's output, a file's contents or a directive's text. It lives at ~/.local/share/rtk/pipeline.log (JSON lines).

text
12:04:11  hook.after_tool.shell   cargo test        term-default    term        3,812 → 640   recall 1f3a9c
12:04:15  hook.before_tool.read   src/Service.java  shunt-large-read route → bulk-read
12:04:31  hook.after_tool.shell   rtk bulk-read …   —               skipped: is_rtk_command

Enable it while debugging a pipeline, then turn it off — it is the input rtk pipeline explain --last reads.

Recovery reporting

RTK counts how often the agent asks for an original back. Reads of the recall store are recorded per tool and reported by rtk gain --recalls and the recovery-reads table in rtk gain --by-method.

It is reporting only: RTK never disables a rule on its own.

rtk gain also breaks savings down --by-rule, --by-method, --by-layer and --shadow; directives appear net of their standing cost and tasks net of worker cost. See Token Savings Analytics.

Sharing a pipeline

share renders a pipeline's measured impact and the pipeline file itself as one artifact. On a terminal it draws a clean, screenshot-able card; piped or redirected — or with --md — it emits the paste-ready markdown card instead (so rtk pipeline share > card.md keeps working), and the two renderings never mix on one stream.

bash
rtk pipeline share              # the active pipeline (terminal card)
rtk pipeline share rtk:default  # a named one
rtk pipeline share --md         # the markdown card, even on a terminal

The card has two panels. What it does is derived from the resolved rules, never hand-written. Measured impact reports percentages per lever — command filtering and agent hooks as the share of a tool's output bytes removed, delegated reads as the absolute context a worker kept out of your window, and prompt-cache as the share of input served from cache. Figures are estimates (≈ bytes/4): ratios are reliable, absolutes approximate. Percentages travel between setups; token counts and costs do not, which is why the card leads with percentages.

Publishing

rtk pipeline share publish is the interactive publisher. It offers two channels and walks you through validate → secret scan → preview of what leaves your machine → one confirmation:

  • GitHub repository (recommended) — one public repo per pipeline, in your account: pipeline.toml plus a README card. The repo description carries the one-line install command, the repo gets the rtk-pipeline topic so rtk pipeline search finds it, and stars on github.com/topics/rtk-pipeline are the leaderboard.
  • GitHub repository (private) — same repo, visible only to accounts you grant access. For a team or your own machines: anyone with access installs it with the same rtk pipeline add github:you/repo line (their gh login provides the access). No topic, no search listing — and private is one visibility toggle away from public, so the secret scan and the warnings apply unchanged.
  • GitHub gist — a single public link with no discovery. Also available non-interactively as rtk pipeline share --gist.

Other hosts: publishing targets GitHub, but installing does not — rtk pipeline add accepts a raw file URL from anywhere (GitLab raw links included), so nothing stops a pipeline living wherever your team keeps code.

bash
rtk pipeline share publish       # interactive: choose repo or gist, confirm, publish
rtk pipeline share --gist        # non-interactive gist; prints: rtk pipeline add <url>

Everything goes through your own gh CLI — RTK never sees or stores a token. If gh is missing or logged out, the publisher prints the exact manual steps instead. Publishing the same pipeline again updates the same repo, so its link and stars survive. If the pipeline was itself installed from someone else's repo, the publisher offers to fork that repo instead of creating a fresh one, keeping the attribution chain visible on GitHub.

What leaves your machine: the pipeline file and the aggregate percentages from your share card. No commands, no paths, no history — and every number is labeled self-reported on the published card.

:::caution[Publishing is public, permanent, and your responsibility]
A repo or gist is public, permanent, and indexable. You are responsible for its contents — RTK does not review or endorse shared pipelines. Your pipeline file is published verbatim, including any [upstream.*] blocks (base_url, api_key_env). Before publishing, a pre-flight scan checks for API-key shapes, tokens, private-key blocks, absolute home paths, and email addresses; a finding blocks publication unless you explicitly type publish anyway (or pass --force). The scan is a safety net, not a guarantee: read the card yourself before sharing it.
:::

Adopting a shared pipeline

add is the other half of share: one command validates a shared pipeline, installs it, and tells you what it needs.

bash
rtk pipeline add github:owner/repo              # a published repo (see `share publish`)
rtk pipeline add github:owner/repo@<ref>        # a specific commit, tag, or branch
rtk pipeline add card.md                        # a saved share card (the TOML is auto-extracted)
rtk pipeline add - < card.md                    # or pasted on stdin
rtk pipeline add https://example.com/p          # or any URL
rtk pipeline add https://gist.github.com/u/ID   # or a gist link from `share --gist`
rtk pipeline add card.md --name flash           # name it (default: the pipeline's own name, or the file stem)
rtk pipeline add github:owner/repo --shadow     # try it silently instead of installing (see below)

A github: source (a plain https://github.com/owner/repo link works too) fetches the repo's pipeline.toml pinned to a resolved commit — the commit is recorded locally, so nothing the author pushes later changes what you installed. A gist.github.com page link is resolved to its raw content automatically.

It validates in memory first — a pipeline that does not parse or type-check installs nothing. On success it writes a named pipeline to ~/.config/rtk/pipelines/<name>.toml (merged with your others, active immediately), then prints any missing auth as the exact export to run, and a rtk llm-proxy start line only if the pipeline actually has proxy rules.

On a terminal, after showing the pipeline's rules, add offers three choices: install now, try silently first (a shadow install — see below), or quit. Re-adding a name that is already installed never replaces it silently: add shows a line diff of the change (and the commit movement, for pinned sources), repeats that you are responsible for what you pull and use, and asks. Non-interactively (pipes, scripts, CI) nothing installs without an explicit opt-in: pass --force to accept the disclosure, or --shadow to try the pipeline without applying anything. The same rule applies to publishing: share --gist in a pipe needs --force, and the interactive publisher asks for an explicit "I understand" after its warning.

Trying a pipeline silently (shadow installs)

--shadow (or answering try silently interactively) installs into a shadow slot: the pipeline is evaluated and measured on your live sessions but applied to nothing — no rewrites, no filtering, no denies, no tasks. After a few sessions, compare and decide:

bash
rtk pipeline eval <name>          # what it would have done, vs your active pipeline, same traffic
rtk pipeline promote <name>       # go live (shows the diff against any installed version, confirms)
rtk pipeline promote <name> --discard   # drop it; nothing was ever applied
text
installed: ~/.config/rtk/pipelines/flash.toml
auth needed: export GEMINI_API_KEY=...   (upstream `flash` — task `bulk-read`)
active now, merged with your other pipelines — see `rtk pipeline show`

The shared file names the environment variable for each key (api_key_env), never the key itself — you supply your own.

:::caution[You are responsible for what you install]
A pipeline directs your agent: it can rewrite the commands it runs, filter and replace what it reads, inject instructions, and send content to any upstream endpoint it declares. add prints exactly what the pipeline does — its rules, directives in full, and every upstream with the env var it would send — before anything runs. Read that summary, especially for a pipeline fetched from a URL.
:::

:::danger[Pipelines that need plugins run third-party native code]
Some pipelines reference plugin methods. A plugin is not sandboxed TOML — it is third-party native Rust that rtk compiles and runs on your machine with your privileges. If a shared pipeline needs a plugin you do not have, add names the missing plugin and refuses to install: to use it you must obtain the plugin's source from its author, add it to your build, and rebuild rtk. If the pipeline references a plugin you already have, add lists it in the disclosure. Either way, install a plugin only if you trust its author and have read its source — you alone are responsible for any native code you build and run.
:::

Discovering shared pipelines

bash
rtk pipeline search              # shared pipelines, most-starred first
rtk pipeline search rust         # filtered by a free-text query

Discovery is GitHub's own index of the rtk-pipeline topic. Each result shows the repo, its stars, and its description (which carries the author's headline number and the install line). The leaderboard is simply github.com/topics/rtk-pipeline sorted by stars. Descriptions and numbers are self-reported by their authors — rtk verifies nothing; rtk pipeline add shows every rule before anything installs, and --shadow lets you measure a pipeline on your own traffic before it touches anything.

Evaluating a pipeline on your own usage

eval answers "would this pipeline do better on my workload?" without adopting it and without storing any conversation content. While armed, every tool output your sessions produce is also run through the candidate's transforms in memory; only the before/after sizes are recorded, the content is discarded, and your agent's output is untouched.

bash
rtk pipeline eval squeeze-term --start   # arm the candidate (must be installed — see `add`)
# ...work normally for a few sessions...
rtk pipeline eval squeeze-term           # compare: candidate vs your active pipeline, same traffic
rtk pipeline eval squeeze-term --stop    # disarm
text
Pipeline eval — `squeeze-term` · last 30 days · vs raw, same trajectory
  candidate      5.1K → 3.2K tokens  (37% removed · 214 outputs)   est.
  active now     5.1K → 4.0K tokens  (22% removed · 214 outputs)   est.
  ~ tokens est. ≈ bytes/4; both measured on the same live outputs
  ~ classifier/route effects excluded from the candidate

Both lines are measured on the same outputs, so the comparison is apples-to-apples. Limits, stated on the report itself: token counts are bytes÷4 estimates; a candidate's classifier- or route-driven rules are excluded (they would need live LLM calls), so eval covers its deterministic transforms.

The continuous counterpart lives in rtk gain: a per-mechanism efficiency section (filter+compress, LLM-spend, recalls, directives, proxy bill actual-vs-without) with a confidence tag on every line — measured where real provider usage exists, est. where it is a size estimate.

bash
rtk methods list    # every registered method with its contract
rtk tasks list      # every configured task with its kind and upstream

Guide Pipelines Conditions (`docs/guide/pipelines/conditions.md`)


title: Conditions and Judges sidebar: order: 3

Conditions and Judges

Operators

Keys combine with AND inside one when. For OR, add a second rule. Operators are suffixes on the key:

Suffix Meaning Example
(none) equals format = "json" · whole_file = true
_in · _not_in membership basename_in = ["Cargo.lock", "yarn.lock"]
_starts_with prefix command_starts_with = "git "
_matches regex command_matches = "^git (diff|show)\\b"
_gt _gte _lt _lte numeric tokens_gt = 300 · file_lines_gte = 350

Tool input fields: input.<field> with the same suffixes inside a nested table — input = { limit_gt = 100 } — and input_missing = ["limit", "offset"].

Hook conditions

A key not listed for the rule's subject and event is refused on a control rule (action = … — E004, the file does not load) and degraded on a transform rule (W015 — the rule can never match, the file still loads). See Errors and Warnings.

Subject before_tool and after_tool after_tool adds
all tool (the category, or the raw tool name under mcp/any), input.*, input_missing, and the call context: intent goal recent_calls format bytes lines tokens duration_ms
read file_path extension basename whole_file (no limit/offset given) — and from disk, before the tool runs: file_exists file_bytes file_lines
write edit file_path extension basename
shell command command_starts_with command_matches is_rtk_command (the command runs through rtk — rewritten, a task, or rtk recall) exit_code
search input.pattern input.path
web url input.query
subagent input.*
mcp server (from mcp__<server>__<tool>), input.*

file_lines and file_bytes are computed under a cost cap (size first; lines counted only below a threshold) so the hook stays within budget on a huge file.

Output format (after a tool): term · json · web · matches (grep-style path:line:content) · lockfile.

MCP output. An mcp rule reads the two response shapes RTK can put back unambiguously: a result string, or a content array whose every block is text. A response carrying an image, an embedded resource or a mixed array is passed through untouched — there is no way to rewrite part of it and leave the rest coherent.

Call context

What the agent was doing when it made the call — the same keys for every agent:

Key Holds Where it comes from
intent the agent's own words about this step (last ~600 chars) Cursor: agent_message. Claude Code, Codex: the latest assistant text or reasoning in the transcript. Otherwise the tool's description
goal the user's latest prompt (first ~600 chars) the transcript
recent_calls the turn's last 5 tool calls, newest first, |-separated the transcript

They are read only when a rule or a judge at that position uses them, and are absent when the agent offers nothing. Claude Code writes its transcript asynchronously, so there intent can be the agent's reasoning from one step earlier — goal and recent_calls are always current.

Test them directly, or let a judge read them:

toml
[pipeline]
extends = "rtk:delegate-reads"

[[hook.before_tool.read]]
name   = "big-reads-not-for-editing"
when   = { file_lines_gt = 400, whole_file = true, intent_matches = "(?i)understand|explain|overview" }
action = "route"
task   = "bulk-read"

Proxy conditions

A block already in the conversation is part of the provider's cached prefix; if RTK treated it differently on one turn than on the previous one, the whole prefix would be re-billed. So a proxy rule may only look at things that are identical on every turn:

Class Keys Legal on
block-intrinsic tool format shape tokens succeeded every part
session-stable model provider session project every part
turn-varying tokens_in message_count stream user and any only — rejected on tool_result, tool_use, assistant, cache (E010)

tool at the proxy uses the same categories as the hooks (shell, read, subagent …). shape is not supplied by the daemon.

Judges — conditions on meaning

A [classifier.<name>] asks a model a typed question and turns the answer into facts a rule conditions on. Three question kinds:

question Fact(s) produced Tested with
choice class.<name> (label) · class.<name>.confidence "class.intent" = "test_failure", _in, .confidence_gte
score score.<name> (0–1) "score.quality_lt" = 0.4 — needs rubric = [..], ordered worst to best
noul noul.<name> (0–1 truth) "noul.is_error_gte" = 0.9
toml
[upstream.jev]
provider     = "typesafe"
model        = "jev-latest"
base_url     = "https://api.typesafe.ai/v1/systemone"
api_key_env  = "TYPESAFE_API_KEY"
timeout_secs = 8

[classifier.intent]
upstream       = "jev"
question       = "choice"
instructions   = "What kind of tool output is this?"
input_chars    = 2000
min_confidence = 0.8

[classifier.intent.labels]
test_failure = "Failing test output: assertions, FAILED markers"
build_log    = "Compiler or build progress output"

[upstream.flash]
provider    = "google"
model       = "gemini-2.5-flash"
api_key_env = "GEMINI_API_KEY"

[task.digest-failures]
kind     = "digest"
upstream = "flash"

[[hook.after_tool.shell]]
name   = "route-test-failures"
when   = { "class.intent" = "test_failure", tokens_gt = 1500 }
action = "route"
task   = "digest-failures"
Key Meaning
upstream an [upstream.*] with base_url and api_key_env, speaking the System One wire format (a label + confidence, not generated text)
question choice, score or noul
instructions what to judge
input_chars how much of the block (or the call) is sent, from the head
min_confidence below this, no fact is produced — rules cannot fire
labels / rubric the answers a choice / score may give
context optional, default false — after a tool, also send the judge the call that produced the output: the tool, its command or path, and the call context. See below

How judges behave:

  • Before or after the tool. After a tool the judge reads the output — and, with context = true, the call that produced it first: the tool, its command or path, size facts and the call context. That is what lets it tell output the agent wants to understand from output it needs verbatim. The call is sent in addition to input_chars of output, so the judge still sees as much output as before. Before a tool it reads the call: the tool, its command or path, size facts (file_lines, file_bytes), every input.* field — for Bash, the agent's own description of why it runs the command — the call context and, for a read, the head of the file. The context is what separates "read to understand" from "read to edit", and it is what lets a judge delegate work before it is done. It costs one judge round trip per call it is asked about (cached afterwards), so put cheap fixed conditions in the same when (file_lines_gt, command_matches) and the judge is only asked about calls worth delegating.
  • Never on deny (E020). Gate denies on fixed conditions.
  • Not under proxy.request. Judges run at hook positions only.
  • Fail-open, always. Missing key, endpoint down, timeout, or confidence under the floor: the fact is absent, absent facts never match, and the call or output goes through untouched.
  • Lazy, batched, memoized. Nothing runs unless an enabled rule at that exact position conditions on a classifier; every question bound to one upstream travels in a single request; answers are cached per content, so repeated output classifies once.
  • Auth is declared, not discovered. The upstream's api_key_env names the credential; rtk pipeline validate, rtk trust and rtk init print auth needed: export VAR=... for anything unset. See Tasks and Upstreams for api_key_command and for borrowing the key your agent already uses through the RTK Proxy.

A judge before the tool

Delegate big reads, heavy commands and documentation fetches only when the judge thinks the agent needs a summary rather than the bytes:

toml
[upstream.judge]
provider     = "laya"
model        = "laya"
base_url     = "http://127.0.0.1:41733/v1/systemone"
api_key_env  = "RTK_LAYA_TOKEN"
timeout_secs = 4

[upstream.haiku]
provider = "anthropic"
model    = "claude-haiku-4-5"

[classifier.skim]
upstream     = "judge"
question     = "noul"
instructions = "The agent is about to run this tool call. Use its intent, the user goal and its recent calls. Answer true if a short answer a smaller model could extract is enough, false if it needs the exact content."
input_chars  = 1500

[task.bulk-read]
kind     = "summarize"
upstream = "haiku"

[[hook.before_tool.read]]
name   = "judge-big-reads"
when   = { file_lines_gt = 300, whole_file = true, "noul.skim_gte" = 0.8 }
action = "route"
task   = "bulk-read"

The full file, which also judges heavy commands and documentation fetches, ships as the judge-before-tool example pipeline; classify-routing is the after-tool counterpart.

For any AI-driven decision, use a classifier.


Guide Pipelines Directives (`docs/guide/pipelines/directives.md`)


title: Directives sidebar: order: 5

Directives

A directive is text RTK writes in front of the agent. It is how RTK reaches what no transform can touch: how the agent writes, which commands it uses.

toml
[[directive]]
use = "terse-output"

[[directive]]
name = "house-rules"
file = ".rtk/directives/house-rules.md"

[[directive]]
use   = "rtk-awareness"
level = "default"
Key Notes
use / text / file exactly one: a shipped directive, inline text, or a file relative to the pipeline file
name defaults to use
channel awareness_file (default): RTK's instruction file, present every turn
enabled
module options level for rtk-awareness

Shipped

Directive Ships What it does
rtk-awareness on the awareness levels (default · high · full); [awareness] in config.toml still applies
terse-output off terse prose; code, paths, commands and errors are never abbreviated, and destructive-action warnings stay full sentences
route-tasks generated written whenever a task has an upstream: tells the agent the exact rtk <task> syntax

A file containing nothing but a directive is a valid pipeline and leaves rtk:default intact — see rtk:terse.

How they land

All enabled directives are rendered and joined in declaration order into one block, written once between RTK's markers in the agent's instruction file. Re-running rtk init rewrites that block and nothing else.

What they cost

A directive sits in context every turn. rtk gain shows directives as net — output tokens saved minus the standing cost.

Trust

A directive changes the agent's behaviour. rtk pipeline show prints every directive verbatim, and rtk init shows a project's directives in full before asking to trust the file.


Guide Pipelines Errors (`docs/guide/pipelines/errors.md`)


title: Errors and Warnings sidebar: order: 9

Errors and Warnings

rtk pipeline validate checks every file and prints each problem with its file, line, rule and a hint:

text
~/.config/rtk/pipeline.toml:41  [[hook.after_tool.shell]] "my-term"
  E003  condition `file_lines` is not available after the tool ran
        hint: `file_lines` is a before_tool condition; for output size use `lines`

Validation never runs inside an agent session. If a file fails to load while the agent is working, RTK drops that file only and records the problem for rtk pipeline status and rtk gain — see Pipeline Commands.

Errors fail closed, warnings load

Errors stop that file from loading; the other files stay active (W025 names the dropped file, and a file that depends on it — a rule routing to a task it defined — is dropped with it). Only an error that cannot be tied to a file falls back to rtk:default. Warnings load.

Transform rules fail open (an unknown method or condition is a warning — W014/W015 — and only that rule goes inert); control rules fail closed (a rule with action = …, and every task kind, must validate completely).

Code Error
E001 unknown section or position ([[hook.during_tool.shell]])
E002 unknown key on a rule, directive, task, upstream or override
E003 condition not available at this event (file_lines after the tool)
E004 condition not available for this subject (command on read) — control rules only; on a transform rule this is W015
E005 action not legal at this position (rewrite outside before_tool.shell, deny after a tool, route before an edit), or a route mode it cannot use (swap outside shell, answer outside read/search/web, mode without route)
E006 a rule has both use and action, or neither
E007 unknown method, chain or directive — for methods, control rules only; on a transform rule this is W014
E008 chain type mismatch (["toon", "cap"]) or a terminal method not last
E010 method under proxy.* is not cache-stable, or a turn-varying condition on a cached part
E011 [override] target does not exist (did-you-mean listed)
E012 new rule declared with an inherited name
E013 the same rule or directive name in two loaded files (both paths listed)
E014 task: unknown kind, unknown upstream, name collides with an rtk command
E015 upstream: literal key (api_key = "…"); use api_key_env
E016 [method.<name>] carries filter keys (match_command, strip_lines_matching, …) — filters live in filters.toml; or a built-in method name carries chain or options (built-in methods take none)
E017 extends: two parents define the same rule name differently
E018 unknown subject or tool category
E019 classifier problem: unknown classifier or label in a condition, question/key mismatch, missing or unknown upstream, bad question or min_confidence
E020 deny gated on a classifier — the protection would vanish whenever the judge is unavailable
Code Warning
W001 rule is unreachable (a catch-all precedes it)
W002 derived name collides — set it explicitly
W003 when = { … } in an override dropped keys: …
W004 deny under write — the body is already generated
W005 proxy.* rules present but the proxy is not running
W006 rtk-rewrite disabled — RTK's primary saving is off
W007 route to a task with no upstream — inert
W008 rule at a proxy.request position that is not active (user, system, tools, model) — the rule is inert
W009 a key, condition or method this edition does not support — the rule is inert; the file still loads
W010 an unsupported key is present ([guards] max_recovery_rate) — ignored; the file still loads
W011 a proxy-only method (cache-align) named outside proxy.* — it is skipped there; looks through [method.*] chains
W012 every method in a rule is skipped at that position — the rule matches and does nothing
W013 use chain or route at hook.after_tool.{write,edit} — no output to extract, the rule is inert
W014 unknown method or chain on a transform rule — skipped at runtime, the rest of the chain still runs; the file still loads (newer rtk or a plugin you don't have?)
W015 unknown condition on a transform rule — the rule can never match on this rtk; the file still loads
W016 method skipped under proxy.request — it needs a recall copy the daemon cannot store, or sits at a part the daemon does not transform; the file still loads
W017 proxy.request rule in a project file — the daemon reads only user pipelines; move it
W018 shadow on a proxy.request rule — not recorded; the rule is skipped
W019 directive channel other than awareness_file — the directive is skipped
W021 route after a read — the summary replaces the bytes a later Edit needs
W022 slow method after a tool — every matching call waits for it
W023 the routed task's kind cannot take this call (see Routing before a tool)
W024 a rule field has the wrong type (enabled = "false", action = 5) — the rule is disabled; the rest of the file still loads
W025 a pipeline file has errors and is not loaded; the other files stay active (the file's own errors are listed with it)

Limits

  • No file contains a command to run. Rewrite is fixed to the rtk prefix; tasks call a model, not a shell. Cloning a repository cannot make RTK execute anything.
  • No file names an agent. Subjects are categories; RTK maps them at rtk init.
  • proxy.* rules need the RTK Proxy in the path. Without it they are inert and validate says so (W005).
  • A route rule needs its task bound to an upstream.
  • There are two scopes: you and the project.

Guide Pipelines Methods (`docs/guide/pipelines/methods.md`)


title: Methods sidebar: order: 4

Methods

A method transforms one block of text. Rules chain them with use = [...];
the chain is checked at load so a method never receives a format it cannot
handle, and a method marked terminal (toon) is only accepted last.

rtk methods list prints this table.

Built-in (hook + proxy sites)

Method Lossy Recovery Runs under proxy.request What it does
ansi no tool_result (normalize) strip escape sequences
progress yes rtk recall (hook site) tool_result (normalize) keep only the last frame of \r-animated output
dedup no tool_result (normalize) collapse ≥3 identical adjacent lines to [xN]
base64-elide yes none — value-free blobs tool_result replace long base64 runs with a size note
cap yes rtk recall <hash> no — needs recall over 300 lines: keep head 50 + tail 200; full output stored
toon yes terminal · rtk recall (hook site) no — needs recall uniform array of flat objects → TOON; else minified JSON
minify-json yes rtk recall (hook site) no — needs recall compact re-serialisation
web-md no tool_result HTML → markdown
md-slim yes none — read-noise tool_result drop badges, comments, blank runs; keeps link URLs
grep-group yes rtk recall (hook site) no — needs recall group path:line:content matches by file; every match kept
lockfile yes none tool_result lockfiles → name@version list
strip-line-numbers yes none; ships off tool_result (normalize) remove line-number prefixes from file reads
truncate yes recall no — needs recall structural line cap (not in any shipped chain)
shape yes recall no — needs recall normalize → classify layout → per-shape render
rle yes rtk recall (hook site) no — needs recall run-length collapse of adjacent identical lines

Every lossless method is idempotent, so a block reaching both a hook rule and
a proxy rule is never damaged; every lossy method marks its output, and the
proxy skips blocks already marked. Methods marked "needs recall" are skipped
under proxy.request because the daemon cannot store the recall copy — see
What the Proxy Compresses.

Proxy-resident

Implemented in the RTK Proxy only.

Method Lossy Recovery Runs under proxy.request What it does
cache-align no cache place the cache breakpoint; no-op if the client set one
ccr yes rtk_retrieve tool_result compress a large tool result already in the conversation to a preview and a marker the model can expand — opt-in
retire-pointer yes the file on disk, rtk_retrieve tool_use replace a file body echoed through a write argument with a pointer to the file — opt-in

Naming it under hook.* leaves the chain without it, so the rule fires as a
no-op. validate reports W011, and W012 when that empties the rule
completely; rtk pipeline show marks the rule (inert: proxy-only).

Chains — shipped composites, usable anywhere a method is

Chain =
term ansi progress dedup base64-elide cap
json base64-elide toon
web ansi base64-elide web-md toon md-slim
matches grep-group

Your own chains

toml
[method.gentle]
chain = ["ansi", "dedup"]

[[hook.after_tool.shell]]
name = "gentle-term"
use  = ["gentle"]

Chains are also composed directly in rules — use = ["ansi", "cap"] runs one
then the other. Order is yours and is checked.

Your own command filters

Custom command filters live where they always have: ~/.config/rtk/filters.toml
(macOS ~/Library/Application Support/rtk/filters.toml) and .rtk/filters.toml
in a project. Nothing about them changes in v1.0 — same keys, same inline
tests, same rtk verify.

toml
schema_version = 1

[filters.mytool]
description          = "Output of my internal deploy tool"
match_command        = "^mytool\\b"
strip_ansi           = true
strip_lines_matching = ["^\\[INFO\\]", "^Downloading "]
max_lines            = 200
on_empty             = "(no output)"

[[tests.mytool]]
name = "strips info"
input = "[INFO] x\nreal line"
expected = "real line"

Then run rtk trust once. Filter files are trust-gated because they rewrite
what your agent sees; an untrusted (or edited-since-trust) file is skipped.

For a command RTK has no filter for, the rtk-rewrite rule rewrites
mytool deploy → rtk mytool deploy (it consults every filter's
match_command), and rtk mytool deploy runs the real command and applies your
filter. Filters are the rewrite's business: they are not methods and do not
appear in use, and the pipeline file does not define them (E016). The
decision log and rtk pipeline explain --last show the rewrite that happened.

Your own methods in Rust

A [method.*] composes existing methods; a plugin adds a
genuinely new transform — or a new task kind — written in Rust and built into
your rtk. It then resolves by name in use = [...] like any built-in.


Guide Pipelines Plugins (`docs/guide/pipelines/plugins.md`)


title: Plugins sidebar: order: 10

Plugins — your own methods and task kinds

A method is the unit of work in a pipeline (ansi, dedup, cap, toon,
…). A plugin is a package you write that provides one or more new methods.
The built-ins cover common output; a plugin is for a transform only you need —
masking a proprietary secret format, collapsing a house log style, reshaping a
bespoke tool's output.

Decisions are different from transforms. If you want an AI/LLM judgment ("is this
worth compressing?", "which model should serve this?"), that is a
[classifier.*] in your pipeline.toml, not a
plugin. Plugins are for transforming a block of text.

The workflow

bash
rtk plugins new shout      # scaffold a method crate in ~/.config/rtk/plugins/
# edit shout/src/lib.rs — fill in apply()
rtk plugins build          # compile a custom rtk with your method linked in
rtk plugins list           # show your plugin crates and what this binary carries

Then name the method in a rule, exactly like a built-in:

toml
[[hook.after_tool.shell]]
name = "loud-errors"
use  = ["shout"]

rtk plugins build generates a small host crate that depends on rtk and your
plugin crates, registers each method, and calls rtk::entry(); it compiles with
cargo and installs the result (a binary still named rtk) to ~/.cargo/bin.
Nothing about the engine, the hooks, or installation changes — a plugin is
purely additive.

What you write

A method implements one trait from the rtk-plugin SDK. The scaffold gives you
an identity transform to fill in:

rust
use rtk_plugin::{rtk_plugin, Block, Method, MethodCtx, MethodInfo, Outcome};

struct Shout;

impl Method for Shout {
    fn name(&self) -> &'static str {
        "shout"
    }

    fn info(&self) -> MethodInfo {
        MethodInfo::LOSSLESS_TRANSFORM
    }

    fn apply(&self, block: &Block, _ctx: &MethodCtx) -> Outcome {
        Outcome::Continue(block.text.to_uppercase())
    }
}

rtk_plugin!(Shout);
  • apply receives the block's text (plus its subject, format, and provenance)
    and returns an Outcome: Continue(String) to pass the transformed text down
    the chain, ShortCircuit(String) to stop the chain, or Skip to leave the
    block untouched.
  • info() is the contract the engine type-checks at load: lossy, recovery
    (how a reader gets the original back, e.g. Recall or None), cost, and what the method consumes/produces. Use
    MethodInfo::LOSSLESS_TRANSFORM as the base and override only what differs —
    a lossy method must declare its recovery so the original can be restored:
    by default from the recall store (rtk recall <hash>), or from files under
    the tee directory if you set [retriever] mode = "tee".
  • rtk_plugin!(…) lists what the crate exports so the build can register it.
    A crate may export several methods: rtk_plugin!(Shout, Whisper);. To export
    task kinds too (or instead), use the long form:
    rtk_plugin!(methods: [Shout], kinds: [Checklist]); — rtk plugins build
    registers both.

rtk pipeline validate checks a plugin method like any other — a bad contract
(a terminal method not last, a Slow method at PreToolUse, a format it cannot
consume) fails at build, with the method name.

Your own task kinds

Methods transform content; task kinds define what a delegated worker does.
The built-ins are summarize (answer a question about files), generate
(write one file from a spec), and digest (condense command output). A plugin
can register a new kind the same way it registers a method:

rust
use rtk_plugin::{rtk_plugin, CorpusFile, TaskIntent, TaskKind, TaskOutcome, WorkerRequest};

struct Checklist;

impl TaskKind for Checklist {
    fn name(&self) -> &'static str { "checklist" }
    fn usage(&self) -> &'static str { "rtk <task> --question \"...\" <files>" }
    fn default_instructions(&self) -> &'static str { "Answer as a checklist." }
    fn assemble(&self, intent: &TaskIntent, corpus: &[CorpusFile],
                instructions: Option<&str>, prompt: Option<&str>)
        -> Result<WorkerRequest, String> { /* build the worker prompt */ }
    fn handle(&self, output: &str) -> TaskOutcome {
        TaskOutcome::Print(output.trim().to_string())
    }
}

rtk_plugin!(methods: [], kinds: [Checklist]);

The rtk_plugin! line is what rtk plugins build wires into the binary. Once registered,
[task.review] kind = "checklist" validates, rtk review
exists as a CLI command, and rules can action = "route" to it. Names must be
unique — built-in kinds cannot be shadowed. A pipeline that declares a task
with a kind your build does not have fails validation (E014).

Upgrading rtk

A plugin is compiled into your custom rtk binary, so installing a new rtk
release replaces that binary and your plugins with it. After every upgrade:

  1. In each plugin's Cargo.toml, set the rtk-plugin dependency's tag to
    the new version (rtk --version → vX.Y.Z). rtk plugins new writes the
    tag once, at scaffold time.
  2. Run rtk plugins build again.

Until you rebuild, a pipeline rule that uses a plugin method fails open: it gets
W014 and is skipped while the rest of the file stays active. A control rule (one
with action =) that depends on it is an error (E007) and its file is not
loaded. rtk pipeline validate shows which rules are affected.

Requirements and notes

  • A Rust toolchain (cargo) on PATH, and network access on the first build —
    the scaffolded Cargo.toml depends on rtk-plugin by git, pinned to the tag
    matching your installed rtk, so your method always links the exact engine
    your rtk runs.
  • A plugin adds methods and task kinds, not conditions. Every field of a
    tool's input is already a condition via the input.* prefix
    (when."input.subagent_type" = "Explore"), and AI-driven conditions are
    classifiers.
  • A project-local .rtk/plugins/ is trust-gated like every other project file;
    review with rtk trust before it is built.

Guide Pipelines Reference Pipelines (`docs/guide/pipelines/reference-pipelines.md`)


title: Reference Pipelines sidebar: order: 7

Reference Pipelines

Printable with rtk pipeline show <name>. Adopt with extends; tune with [override].

`rtk:default` — RTK exactly as it works today

What every user has after upgrading, with no action. One hook event (PreToolUse), the awareness file, your filters.toml and config.toml. Nothing else is registered or changed.

It also carries one proxy.request rule, normalize, which does nothing unless you are running the daemon. It strips ANSI codes, progress bars and repeated lines — bytes no reader ever sees — and each of its methods is a pure function of the block's own content, so the request prefix stays byte-identical across turns and the upstream prompt cache keeps hitting.

toml
schema_version = 1
[pipeline]
name = "rtk:default"

[[hook.before_tool.shell]]
name   = "rtk-rewrite"            # `ls` → `rtk ls` for every command RTK has a filter for; others untouched
action = "rewrite"

[[proxy.request.tool_result]]
name = "normalize"                # inert without a running daemon
use  = ["ansi", "progress", "dedup"]

[[directive]]
use = "rtk-awareness"
level = "default"     # today's [awareness] level in config.toml still applies

[guards]
never_worse = true
min_size = 512

How the rewrite flows: your agent calls Bash "ls -la" → the hook evaluates hook.before_tool.shell → rtk-rewrite matches → the call becomes rtk ls -la → inside rtk, the ls filter (built-in, or yours from filters.toml) applies. Excluding a command is a passthrough rule ahead of rtk-rewrite, or [hooks] exclude_commands in config.toml as before.

Hooks follow the pipeline. rtk init registers only the events your active pipeline uses, for the tools its rules name. With rtk:default that is PreToolUse on Bash, as today. Enable rtk:posthook and PostToolUse is registered too. Registration happens at rtk init: after adding a rule at a new event or for a new tool (a hook.before_tool.read rule, any hook.after_tool rule), run rtk init -g again. Until you do, rtk pipeline validate, rtk gain and rtk init --show print hook needed: ….

`rtk:posthook` — compress every tool's output

Opt in. Adds PostToolUse: after a tool runs, its output is compressed before the agent reads it. Only representation changes; nothing is dropped except by cap, which keeps the full output for rtk recall.

toml
[pipeline]
name = "rtk:posthook"
extends = "rtk:default"

[[hook.after_tool.shell]]
name = "term-default"             # floor for output that did not come through rtk
when = { is_rtk_command = false }
use  = ["term"]

[[hook.after_tool.read]]
name = "lockfiles"
when = { basename_in = ["package-lock.json", "Cargo.lock", "yarn.lock", "pnpm-lock.yaml"] }
use  = ["lockfile"]

[[hook.after_tool.read]]
name = "json-reads"
enabled = false
when = { format = "json" }
use  = ["json"]

[[hook.after_tool.read]]
name = "strip-line-numbers"
enabled = false
use  = ["strip-line-numbers"]

[[hook.after_tool.search]]
name = "grep-group"
when = { format = "matches" }     # grep output; glob file lists are not matches and pass untouched
use  = ["matches"]

[[hook.after_tool.web]]
name = "web-default"
use  = ["web"]

`rtk:proxy` — cache alignment in the proxy

Opt in; needs the RTK proxy running (rtk init offers to install it). Normalisation
(ansi, progress, dedup) is inherited from rtk:default, so what this pipeline adds on top
is the cache breakpoint.

Both are Anthropic-dialect only, and cache-align does nothing for a client
that already sets its own cache_control breakpoints — Claude Code does. An
OpenAI-dialect client is measured but not compressed (Provider dialects).

toml
[pipeline]
name = "rtk:proxy"
extends = "rtk:default"

[[proxy.request.cache]]
name = "align"
use  = ["cache-align"]

`rtk:delegate-reads` — route oversize reads to a cheaper model

toml
[pipeline]
name = "rtk:delegate-reads"
extends = "rtk:default"

[task.bulk-read]
kind = "summarize"    # no upstream: you supply the model
[task.code-write]
kind = "generate"

[[hook.before_tool.read]]
name   = "shunt-large-read"
when   = { file_lines_gt = 350, whole_file = true }
action = "route"
task   = "bulk-read"

Adopt it as in the third example on the overview. Tune: [override.shunt-large-read] when.file_lines_gt = 500; [task.bulk-read] temperature = 0.2 instructions = "…". code-write has no rule — denying a write wastes tokens already generated — the agent learns it from the generated route-tasks directive. A cat big.java is covered too: it is rewritten to rtk read big.java, which evaluates the same rule.

Routing a command, and routing output (how routing works). The same action covers the other two
anchors. Neither is shipped in a built-in — write them yourself:

toml
[pipeline]
name = "delegate-commands"
extends = "rtk:posthook"

[upstream.flash]
provider     = "google"
model        = "gemini-2.5-flash"
api_key_env  = "GEMINI_API_KEY"
timeout_secs = 8

[task.check-tests]
kind     = "digest"
upstream = "flash"

[[hook.before_tool.shell]]           # the command never runs in your context
name   = "delegate-tests"
when   = { command_matches = "^(npm|pnpm) test\\b" }
action = "route"
task   = "check-tests"

[[hook.after_tool.web]]              # the fetch already happened; digest it
name   = "digest-pages"
when   = { tokens_gt = 4000 }
action = "route"
task   = "check-tests"

The first denies the call and tells the agent to run
rtk check-tests --question "…" -- npm test; the exit code still comes from
npm test. The second sends the fetched page to the worker and returns its
bullets with a rtk recall hint for the original.

Prefer a transform chain where one is enough. use = ["term"]
costs no time and no tokens and keeps the whole output recoverable; an
after_tool route spends a provider round trip — seconds, on the hook path —
to throw detail away. It earns its keep on output that is both very large and
genuinely disposable, and timeout_secs is what bounds the damage when the
worker is slow.

`rtk:verbatim-diffs` — no line numbers, diffs untouched

toml
[pipeline]
name = "rtk:verbatim-diffs"
extends = ["rtk:posthook", "rtk:proxy"]

[override.strip-line-numbers]     # for agents whose edit tools do not address lines by number
enabled = true

[[hook.before_tool.shell]]        # diffs must reach the agent byte-for-byte
name   = "keep-diffs"
when   = { command_matches = "^git (diff|show)\\b" }
action = "passthrough"

Selective compression with a recovery path and grep grouping come from rtk:posthook (term, matches). It also inherits rtk:proxy, which adds the cache breakpoint (see rtk:proxy).

`rtk:terse` — terse prose

toml
[pipeline]
name = "rtk:terse"
extends = "rtk:default"

[[directive]]
use = "terse-output"

A directive only: it changes what the agent writes, never what it reads, so it
composes with anything. For terse prose and output compression, name both —
extends = ["rtk:terse", "rtk:posthook"]. A directive is also the one thing you
can adopt without a pipeline at all: a file containing nothing but
[[directive]] use = "terse-output" is valid and leaves rtk:default intact.


Guide Pipelines Tasks (`docs/guide/pipelines/tasks.md`)


title: Tasks and Upstreams sidebar: order: 6

Tasks and Upstreams

A task is a command the agent can run that hands work to a cheaper model. RTK ships three kinds — interfaces with a prompt template and output handling. You create named instances of a kind and bind each to a model (an upstream). A plugin can register further kinds; a task whose kind this rtk does not have fails validation (E014) — tasks sit on the control path.

Kind Interface The agent types Saves on
summarize files + a question → bullets rtk <task> --question "…" read
digest captured output + a question → bullets rtk <task> --question "…" -- <command> shell, search, web, mcp
generate a spec + a reference file → a file written; the agent sees one line rtk <task> --spec "…" --reference <f> --target <f> write, read
toml
[upstream.flash]
provider     = "google"
model        = "gemini-2.5-flash"
api_key_env  = "GEMINI_API_KEY"
timeout_secs = 30

[task.bulk-read]
kind         = "summarize"
upstream     = "flash"
temperature  = 0.2

[task.code-write]
kind     = "generate"
upstream = "flash"

The agent reaches a task two ways: a route rule hands it a call (Routing before a tool), or the generated route-tasks directive tells it the command exists.

Tasks

Key Meaning
kind summarize · digest · generate, or a plugin kind
upstream the [upstream.*] it calls. Without one the task is inert, every rule routing to it is inert too (W007), and explain says so
temperature optional
instructions optional — the worker's system prompt; default is the kind's
prompt optional — assembly template: ${question} ${files} / ${spec} ${reference}
context optional, default false — when RTK runs the task itself (a route after a tool, or a route with mode = "swap" or "answer"), ask the worker a question built from what the agent is doing: the user's request, the agent's current step and its recent calls. See below

The task name is what rules target and what the agent types. A name that collides with a built-in rtk command does not load (E014).

Resolution: rtk bulk-read … → [task.bulk-read] → kind (interface, template, output handling) → upstream (model, key) → call → output.

The prompt: system = the instance's instructions or the kind's default; user = the template with the agent's intent (--question / --spec) and the file contents RTK reads from disk. The files never enter the agent's context — that is the saving. generate refuses to run without --reference: without a file to match, the worker writes context-free code.

The question when RTK runs the task. When the agent types the command, its --question is the question. When RTK runs the task on the agent's behalf — a route after a tool, or a route mode swap or answer — nobody wrote one. With context = true the worker gets the agent's call context, so it keeps what the agent was looking for:

text
Question: The user asked: fix the failing build
The agent's current step: find which module fails and why
Its recent tool calls: shell mvn -q package | read pom.xml
What in this does the agent need for its current step? Keep exact names, numbers, paths, line numbers and error lines.

Without it, the worker gets only the last line. The context costs a few hundred input tokens per call. On Claude Code the agent's current step can lag one step behind, because the transcript is written asynchronously; the user's request and the recent calls are always current.

Personas, not kinds: a "review" task is [task.review] kind = "summarize" instructions = "…". New personas are three lines; new kinds are plugins.

Upstreams

An upstream is a model endpoint, shared by tasks and judges.

Key Meaning
provider anthropic · openai · google for a task; a judge upstream is reached through its base_url, whatever its provider says
model the model name
base_url optional — any compatible endpoint
api_key_env the environment variable holding the key. Omit it for the provider's conventional variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY), which validate names for you. A literal api_key = "…" does not load (E015)
api_key_command or run a command and take its stdout (keychain, vault) — the same shape as Claude Code's apiKeyHelper. It runs through sh on Linux/macOS and cmd on Windows, so a command written for Git Bash needs rewriting there, or an explicit bash -c "…". An upstream with one is never reported as needing auth
timeout_secs default 30; set it lower for anything on the hook path

rtk pipeline validate, rtk trust and rtk init print auth needed: export VAR=... for any key that is unset.

Already-authenticated agents need no second key. When the RTK Proxy is running and the upstream names a provider it speaks (anthropic, openai), a worker with no credential of its own is routed through the daemon, which lends the credential the agent is already relaying. An agent set up with rtk init delegates with nothing further to configure. A request that brings its own key is never substituted; with no daemon up the worker goes direct and a missing key is still an error. To refuse borrowing, set RTK_PROXY_RELAY_BORROWING=false on the daemon.

Cost

The worker's tokens are paid too. rtk gain shows saved − worker cost, net. Calls go directly to the provider, or through the RTK Proxy when it is running, in which case they are measured there.


Guide Pipelines Writing Pipelines (`docs/guide/pipelines/writing-pipelines.md`)


title: Writing a Pipeline sidebar: order: 2

Writing a Pipeline

The file

Where it lives, what wins

File Scope Notes
rtk:default, rtk:posthook, rtk:proxy, rtk:delegate-reads, rtk:verbatim-diffs, rtk:terse built in read-only; compiled into the binary — see Reference Pipelines
~/.config/rtk/pipeline.toml you loads first
~/.config/rtk/pipelines/*.toml you every file loads, alphabetically — name them 10-…, 20-… if order matters
.rtk/pipeline.toml this project trust-gated — rtk init in the project shows what it contains and asks; a changed file asks again
.rtk/pipelines/*.toml this project every file loads, alphabetically; each trust-gated
~/.config/rtk/filters.toml, .rtk/filters.toml you · project your custom command filters, unchanged — see Methods

Paths use the Linux spelling. The user directory is the platform config dir: ~/.config/rtk/ on Linux, ~/Library/Application Support/rtk/ on macOS, %APPDATA%\rtk\ on Windows.

Load order: built-ins → your pipeline.toml → your pipelines/ → the project's pipeline.toml → the project's pipelines/. Later wins. One file is the simple case; a directory is how you keep several pipelines and turn them on by dropping them in.

Across files: definitions ([task], [upstream], [method]) merge by key; [override] applies later-wins per field; a rule or directive name that appears in two loaded files is an error (E013) naming both paths — nothing shadows silently, and rtk pipeline show prints the source file of every line. Files do not extend each other: they are all loaded, so to reuse a piece, give it its own file.

proxy.request rules belong in your user files: one daemon serves every project, so it never reads a project's .rtk/ pipeline (W017).

config.toml is unchanged and separate: preferences ([tracking], [display], [hooks], [retriever], [telemetry], [limits], [awareness]) stay there. The pipeline holds behaviour; it never replaces an existing RTK file.

Layout

text
schema_version = 1
[pipeline]                         # name, extends, enabled

[[hook.before_tool.<subject>]]     # rules — before a tool runs
[[hook.after_tool.<subject>]]      # rules — after a tool ran
[[proxy.request.<part>]]           # rules — the request, about to be sent

[[directive]]                      # what RTK tells the agent
[method.<name>]                    # chains, options for built-in methods
[classifier.<name>]                # AI judges rules can condition on
[task.<name>]                      # delegation commands
[upstream.<name>]                  # model endpoints
[override.<name>]                  # patch an inherited rule or directive
[guards]                           # global safety limits

A minimal file uses one or two of these.

`[pipeline]`

Key Value
name optional for your files; rtk:* for built-ins
extends a built-in name or a list of them: "rtk:default", ["rtk:delegate-reads", "rtk:terse"]. Left to right; a shared ancestor is inherited once; the same rule name from two parents is an error unless the rules are identical. Built-ins only — local files compose by being loaded, not by extending each other. Omit for a standalone file.
enabled false turns the whole file off without moving it. Default true.

The tree — where a rule lives

A rule's position says three things, in this order:

Segment Answers Values
site which RTK mechanism runs it hook · proxy
event when before_tool · after_tool · request
subject on what a tool category (hook) · a part of the request (proxy)

The position fixes what the rule can see and do. file_lines exists before a tool runs (the file is on disk); format exists after (there is output). A rule that names a condition its position cannot provide does not load — see Conditions and Judges.

Hook subjects

You write the category; RTK maps your agent's tool names to it.

Subject Meaning Claude Code tools it covers
read read a file Read, NotebookRead, TaskOutput
write create or overwrite a file Write
edit modify a file Edit, MultiEdit, NotebookEdit
shell run a shell command Bash, PowerShell, Monitor
search search files or content Grep, Glob, LSP
web fetch or search the web WebFetch, WebSearch
subagent spawn or message a sub-agent Task, Agent, SendMessage, ListAgents, Workflow
mcp any MCP tool mcp__*, ToolSearch, WaitForMcpServers, ListMcpResourcesTool, ReadMcpResourceTool
any fallback — evaluated only if no rule under the specific subject matched all

Other agents are mapped the same way; rtk pipeline explain shows which category a call was assigned, and Agent Coverage lists what each agent supports.

Proxy parts

Part What the rule sees
tool_result one tool-result block in the conversation
tool_use one tool-call block (the agent's request, including a written file's body)
assistant one assistant message
cache cache breakpoint placement
any any block; fallback

What the proxy actually runs at these parts is on What the Proxy Compresses.

The token layers, for reference

If you think in the layers Claude Code's /context shows, this is where each is configured. Layers are how rtk gain --by-layer reports; they are never written in the file.

Layer Where
Tool results hook.after_tool.{shell,search,web,subagent,mcp} (rtk:posthook) · proxy.request.tool_result
Read hook.before_tool.read · hook.after_tool.read
Write proxy.request.tool_use · the code-write task
History proxy.request.tool_result · proxy.request.assistant
Cache proxy.request.cache
Agent message directive terse-output
Skills / MCP · User prompt directives
Inference route to a task before any tool, or after any tool with extractable output, optionally gated on a judge

Rules

Anatomy

toml
[[hook.after_tool.shell]]
name     = "term-default"
when     = { is_rtk_command = false }
use      = ["term"]
continue = false
enabled  = true
shadow   = false
Key Required Notes
name yes* unique across the resolved pipeline. *Defaults to the first use entry when that is unambiguous; a derived name that collides with another rule is a warning — set it explicitly
when no conditions, all must hold; omitted = matches everything
use / action exactly one use is a chain of methods; action is control flow (below)
task · upstream · message with route / deny see Actions
mode no route before a tool only: suggest (default) · swap · answer — see Route modes
continue no default false — first match wins; true keeps evaluating later rules
enabled no default true; a disabled rule is skipped but still shown by rtk pipeline show
shadow no transforms only; decide and record, emit nothing — see Shadow mode

Order

Within one [[site.event.subject]] list: the project's rules first, then yours, then inherited rules — within a scope, files in load order, then declaration order. First match wins unless continue = true. The any list is evaluated only after the specific subject's list matched nothing.

rtk pipeline validate warns when a rule can never fire — for instance a rule after one with no when and no continue in the same list (W001).

Actions

What a rule may do depends on its position:

Position use = [...] action = Extra keys
hook.before_tool.shell — rewrite · deny · passthrough · route task with route; message optional
hook.before_tool.read — deny · passthrough · route task with route; message optional
hook.before_tool.write — deny (warned: the body is already generated; a deny only buys a retry) · passthrough · route task with route
hook.before_tool.edit — deny · passthrough
hook.before_tool.{search,web,subagent,mcp,any} — deny · passthrough · route task with route
hook.after_tool.{shell,search,web,mcp,subagent,any} transform chain passthrough · route task with route
hook.after_tool.read transform chain passthrough · route (W021: breaks a later Edit) task with route
hook.after_tool.{write,edit} — (no output to extract, W013) passthrough
proxy.request.{tool_result,tool_use,assistant,any} transform chain — cache-stable methods only —
proxy.request.cache ["cache-align"] —

rewrite — prefix the command with rtk when RTK has a filter for it (ls → rtk ls); leave it untouched otherwise. It never takes a target or options. Its settings ([hooks] exclude_commands, transparent_prefixes) stay in config.toml.

deny — refuse the call. message is shown to the agent; ${file_path}, ${file_lines}, ${command} interpolate. A deny can only depend on fixed conditions, never on a judge (E020).

passthrough — stop here, do nothing. Before a tool: exempt the command from rewrite. After: skip compression for this output.

route + task — hand the work to a cheaper model (Tasks and Upstreams). Where the rule sits decides how.

Routing before a tool

The call has not run, and no agent lets a hook return a result, so RTK denies the call with a generated message naming the task — "Read of src/Service.java (4,120 lines) is blocked. Run: rtk bulk-read --question "<what you need to know>" src/Service.java", or for a command "npm test is handled by a worker model. Run: rtk check-tests --question "<what you need to know>" -- npm test" — and the agent runs it. The agent supplies the question because only the agent knows why it wanted the file or the command. Override the text with message. A routed command still exits with the command's own code.

What each call is handed over as, and the task kind that takes it (a mismatch is W023):

Before Handed over as Task kind
shell the command: -- <command> digest
read the file path summarize or digest
web -- curl -sL digest
search -- rg -n '' digest
write --spec … --reference … --target <file> generate
subagent its description as the question, over files the agent names summarize
mcp, any a question only a plugin kind

Gate it on a judge to delegate only the calls worth it.

Route modes

By default a route before a tool suggests the task, and the agent spends a turn running it. mode lets the rule deliver the cheaper answer in the same turn instead:

mode What happens Extra turn Works on
suggest (default) the call is refused with a message naming the task; the agent decides to run it yes every subject, every agent
swap the command is replaced by the task wrapping it — rtk <task> --question "…" -- sh -c '<command>' — and runs as the agent's own call no shell
answer RTK runs the task itself and gives the agent the answer in place of the call no read, search, web
toml
[upstream.haiku]
provider = "anthropic"
model    = "claude-haiku-4-5"

[task.bulk-read]
kind     = "summarize"
upstream = "haiku"
context  = true

[task.check-build]
kind     = "digest"
upstream = "haiku"
context  = true

[[hook.before_tool.read]]
name   = "answer-big-reads"
when   = { file_lines_gt = 400, whole_file = true }
action = "route"
task   = "bulk-read"
mode   = "answer"

[[hook.before_tool.shell]]
name   = "swap-builds"
when   = { command_matches = "^mvn " }
action = "route"
task   = "check-build"
mode   = "swap"

With swap and answer there is no agent writing the question, so RTK asks one. With context = true on the task, it is built from what the agent is doing — the user's request, the agent's current step and its recent calls. Without it, a generic question asks for what matters, keeping exact names, numbers and error lines.

  • swap needs nothing from the hook: the agent's own call runs the task, and the command keeps its exit code. If the worker fails, the command's own output is shown. Agents that cannot replace a command show the swapped command as a suggestion instead.
  • answer waits for the worker inside the hook, bounded by the upstream's timeout_secs — set it low. If anything fails (no key, timeout, empty answer), the original call runs as if the rule had not matched. After an answered read the agent can still read exact lines with offset and limit, since a ranged read is not whole_file.
  • Any other combination is refused (E005): swap is for commands only, and answer on a shell command is what swap does without blocking the hook.

Routing after a tool

On any subject with extractable output the output already exists, so there is nothing to ask the agent to re-run: RTK sends the output to the task and puts the answer in its place, with the original kept for rtk recall. The task must be a digest instance. Set context = true on it so the worker knows what the agent was looking for; otherwise it answers a generic question.

This is the one rule that makes a provider call on the hook path — bound it with the upstream's timeout_secs (default 30 s; set it lower for the hook path). Every failure — no upstream, refused, timed out, empty answer — passes the original output through untouched.

route after a read is allowed but warned (W021): a later Edit needs the exact bytes, so keep it to files the agent only reads. After write and edit there is no output to replace (W013).

Overrides

Everything you inherit has a name. [override.<name>] patches one inherited rule or directive in place; fields you do not mention are kept.

toml
[pipeline]
extends = ["rtk:delegate-reads", "rtk:posthook"]

[override.shunt-large-read]
when.file_lines_gt = 500

[override.strip-line-numbers]
enabled = true

[override.term-default]
use = ["ansi", "dedup"]

[override.rtk-awareness]
level = "full"
Field Semantics
enabled action task upstream message continue shadow and module options replaced
when.<key> = v patches one key; the rest of when is kept
when = { … } replaces the whole condition — validate warns and lists the keys you dropped (W003)
use replaced whole
name, position in the tree not overridable — that is a new rule

Overriding a name that does not exist (E011), or declaring a new rule with an inherited name (E012), does not load. Project overrides beat user overrides beat built-ins. rtk pipeline show prints where each value came from.

Add vs change: to change how an inherited rule behaves, override it. To add behaviour, write a new rule with a new name — it is evaluated before the inherited ones and can shadow them for what it matches.


Guide Proxy Compression (`docs/guide/proxy/compression.md`)


title: What the Proxy Compresses sidebar: order: 2

What the Proxy Compresses

The proxy runs the proxy.request.* rules of your pipeline on every request your agent sends, before it is forwarded. Those rules are written in the same pipeline.toml as your hook rules — see Writing a Pipeline for the parts a rule can target.

proxy.request rules belong in your user pipeline: one daemon serves every project, so it never reads a project's .rtk/ files (warning W017). A pipeline change takes effect when the daemon restarts.

Proxy rules must be cache-stable

A block already in the conversation is part of the provider's cached prefix. If RTK treated it differently on one turn than on the previous one, the whole prefix would be re-billed. So a proxy rule may only use cache-stable methods and conditions that are identical on every turn (error E010 otherwise). The condition classes are listed in Conditions and Judges.

What ships on

Normalisation (rtk:default, rule normalize): ansi, progress, dedup on every tool result. It removes bytes no reader ever sees, and each method is a pure function of the block's own content, so the prefix stays byte-identical across turns. It does nothing unless the daemon is running.

Cache alignment (rtk:proxy, rule align): cache-align places the cache breakpoint on the stable prefix. It does nothing for a client that already sets its own cache_control breakpoints — Claude Code does.

Both are Anthropic-dialect only; see Provider dialects.

Your own methods on tool results

The daemon evaluates your proxy.request.tool_result rules per tool result — rule order and when honoured, conditions limited to the cache-safe keys — and runs the selected chain. web-md, md-slim, base64-elide, lockfile and your own [method.*] chains of them work there.

toml
[[proxy.request.tool_result]]
name = "slim-web-pages"
when = { tool = "web" }
use  = ["web-md", "md-slim"]

Two limits apply:

  • Read results are never rewritten — a later Edit needs the bytes on disk.
  • Methods whose only recovery is a recall copy are skipped (cap, toon, minify-json, rle, grep-group, truncate): the daemon cannot store that copy. Use them under hook.after_tool instead. rtk pipeline validate reports W016 for a method that will be skipped, and for an engine method placed at a part the daemon does not transform (tool_use, assistant).

Large tool results: CCR

Off until a rule names it. ccr compresses a tool result that is already in the conversation: the block becomes a head-and-tail preview ending in a marker,

text
[rtk-proxy: compressed N tokens — call rtk_retrieve("<id>") to get the full content]

and an rtk_retrieve tool is added to the request. When the model calls it, the daemon answers from its own store inside the same request, so your agent never sees the round trip. The marker is identical on every later turn, so the cached prefix is not disturbed.

toml
[[proxy.request.tool_result]]
name = "ccr"
when = { tokens_gt = 1500 }
use  = ["ccr"]

Name the rule ccr: its tokens_gt is the floor below which a result is left alone (1500 tokens if the rule has none). Read results are never compressed. RTK_PROXY_CCR=false keeps the method off even when a rule names it.

What it stores. To answer rtk_retrieve, the daemon keeps the original text of every result it compressed in rtk-proxy-memory.db, next to the usage database and readable by your user only, for 7 days (icm_ttl_secs). This is the one case where the proxy keeps request content on disk. Delete the file to wipe it; the daemon recreates it empty.

retire-pointer, at proxy.request.tool_use, is the companion for a file body echoed back through a Write or Edit argument: the write already succeeded, so the argument becomes a pointer to the file and the agent's own Read is the recovery. Same store, same marker; a rule named retire-writes sets its floor (400 tokens if none).


Guide Proxy Configuration (`docs/guide/proxy/configuration.md`)


title: Proxy Configuration sidebar: order: 3

Proxy Configuration

Settings

Precedence: CLI flag > environment variable > config file > default. Every key mirrors its environment variable one to one.

Key Env Default What it does
bind RTK_PROXY_BIND 127.0.0.1:8080 Listen address
ccr RTK_PROXY_CCR true Permission only: CCR runs when a rule names it (Large tool results); false keeps it off regardless
anthropic_upstream RTK_PROXY_ANTHROPIC_UPSTREAM https://api.anthropic.com Where /v1/messages goes
openai_upstream RTK_PROXY_OPENAI_UPSTREAM https://api.openai.com Where /v1/chat/completions goes
db RTK_PROXY_DB rtk-proxy.db Usage store — the database rtk gain reads
relay_borrowing RTK_PROXY_RELAY_BORROWING true Lets a pipeline task with no key of its own borrow the credential your agent is already relaying (Tasks)
bash
rtk llm-proxy config get

The upstreams cannot be set over HTTP; cache_align and normalize are boot-only. A pipeline change takes effect on restart (rtk llm-proxy stop && rtk llm-proxy start) — the daemon does not watch the file.

There is no body-logging switch: the daemon does not store prompt or completion text. The one exception is opt-in — CCR keeps the originals it compresses, see Large tool results.

:::caution[Keep it on loopback]
The proxy forwards your provider credentials upstream. It is built to be a single-operator local process: service install refuses a non-loopback bind outright, and a foreground start warns. If you bind it to a network, put a boundary in front of it.
:::

Provider dialects

Dialect Route Measured Compressed
Anthropic Messages /v1/messages yes yes
OpenAI Chat Completions /v1/chat/completions yes no

Every shipped transform is Anthropic-only. An OpenAI-dialect client is forwarded unchanged — usage, cache-hit accounting and rtk gain still work, compression does not.

HTTP endpoints

Route Purpose
POST /v1/messages Anthropic Messages API — intercept and forward
POST /v1/chat/completions OpenAI Chat Completions — forward and measure; not compressed
GET /health Liveness (what rtk llm-proxy status polls)
GET /stats/{summary,by-compression} Usage reads — aggregate counts only, no request content
GET / PUT /admin/settings Runtime settings. PUT requires the x-rtk-proxy header and does not accept the upstream URLs
POST /admin/shutdown Graceful shutdown (what stop calls). Requires the x-rtk-proxy header
/v1/messages/* (e.g. count_tokens) Relayed verbatim to the Anthropic upstream
anything else 404 with a JSON routing_unknown error — never relayed

A client points its whole base URL at the proxy, so it also calls sibling endpoints. Paths under /v1/messages/ go to the one upstream that dialect is bound to. Any other path gets a JSON 404 instead of being relayed: guessing an upstream would send your API key to whichever provider was guessed. A client that needs other endpoints should talk to its provider directly.

Request bodies are accepted up to 64 MB — long conversations and big tool results routinely exceed the usual 2 MB default.

There is no PID file and no signal handling: stop, status, stats and config are HTTP clients, so they work however the daemon was launched.

Attribution headers

Both optional, both consumed by the proxy and never forwarded upstream:

Header Effect
x-rtk-session Tags the usage row with a session. Without it, rows share one default session.
x-rtk-project Tags the usage row, so savings can be attributed per codebase.

Guide Resources Agent Coverage (`docs/guide/resources/agent-coverage.md`)


title: Agent Coverage sidebar: order: 6

Agent Coverage

A pipeline.toml is written once. How much of it actually runs depends on what your agent's hook API lets RTK do — and that varies a lot.

Where a column says "—", the agent does not offer the capability.

The table

Rows are grouped by what works — output compression first, then command rewriting without compression, then partial coverage — and alphabetically within each group.

Agent Rewrite the command Block it Compress tool output Add context Session start Route via proxy
Claude Code yes yes yes yes yes yes
Copilot CLI yes yes yes ¹ yes yes yes
Hermes yes yes yes yes yes yes
Kilo Code ⁸ yes yes yes yes — yes
Mistral Vibe yes yes yes ³ yes — —
Oh My Pi yes yes yes yes yes yes
OpenCode yes yes yes ² yes — —
Pi yes yes yes yes yes —
Cursor yes yes MCP only yes yes —
Codex yes ⁵ yes — yes yes yes
Copilot (VS Code) yes yes — yes yes —
Factory Droid yes yes — yes yes —
Gemini CLI yes ⁴ yes — yes yes yes
OpenClaw yes yes — — yes yes
Trae yes yes — yes yes —
Windsurf / Devin ⁸ yes ⁶ yes — yes yes —
Antigravity ⁸ — yes — — — —
Cline ⁸ — ⁷ yes — yes — yes
Kimi Code ⁸ — yes — — yes yes
Roo Code — — — — — yes

Command rewriting — turning git status into rtk git status — is allowed by 16 of 20; rtk init installs it on 14. That is RTK's main mechanism, and it is where most of the savings come from.

Compressing tool output after the fact is allowed by 8; rtk init installs it on 7. Everywhere else the model reads the raw result, because the agent provides no way to replace it.

Footnotes — where the detail matters

  1. Copilot CLI: output replacement relies on Copilot CLI's documented modifiedResult field.
  2. OpenCode: built-in tools only. MCP results are rebuilt after the hook runs, and built-in output is already truncated by OpenCode before RTK sees it.
  3. Mistral Vibe: replacement rides on a deny decision, reaches only the text form of the result, and the UI shows it as a warning.
  4. Gemini CLI: input edits merge rather than replace, and each one makes Gemini append a [System] Tool input parameters were modified note to the output — so rewriting costs a few tokens back.
  5. Codex: the rewrite is honoured only alongside an explicit allow decision. Without it the hook fails and the original command runs unwrapped.
  6. Windsurf: now Devin Desktop. Older Windsurf installs are block-only and cannot rewrite.
  7. Cline: the VS Code extension silently drops input overrides and cannot replace output. Both work only in the SDK/CLI plugin runtime, which is a different install. Hooks are also off by default — enable them in Feature Settings.
  8. Rules file only: for Kilo Code, Cline, Windsurf / Devin, Kimi Code and Antigravity, rtk init writes a rules file asking the model to prefer rtk <cmd> and installs no hook. Their row shows what the agent's hook API allows; with RTK today, nothing is blocked, rewritten or compressed.

What this means per pipeline position

Position Where it runs
hook.before_tool.shell every agent with a hook
hook.before_tool.{write,edit,search,web,subagent,mcp,any} every agent with a hook
hook.before_tool.read Claude Code
hook.after_tool.* Claude Code
proxy.request.* any agent you can point at the daemon (the last column above)

A non-shell rule can deny or passthrough. It cannot rewrite the call — that
is a shell capability, because rewriting a command is what RTK does.

Each subject sees the facts its conditions need, plus input.* mirroring the
tool's own arguments so a rule can reach a field RTK has no named condition for:

toml
[[hook.before_tool.edit]]
name    = "no-lockfile-edits"
action  = "deny"
message = "do not hand-edit lockfiles"
when    = { basename = "Cargo.lock" }

[[hook.before_tool.mcp]]
name   = "one-server-only"
action = "deny"
when   = { tool_starts_with = "mcp__payments__" }

The tool fact is the category — edit, web, search — so one rule covers
agents that spell the tool differently. Under mcp and any it is the raw tool
name instead, which is what lets a rule target a single MCP server.

read keeps its own evaluator because its conditions (file_exists,
file_bytes, file_lines) are read from disk before the tool runs.

Agents that cannot be made agnostic

Three are limited by the agent's own hook API:

  • Antigravity — hooks can block a call but never rewrite its arguments, PostToolUse returns {} so output is immutable, and there is no base-URL override. Deny-only.
  • Kimi Code — the hook API is allow/deny with no mutation anywhere. Its API traffic is proxyable, so the proxy layer is the route.
  • Roo Code — has no hooks. Only the awareness file and an OpenAI-compatible base URL are available.

Checking your own setup

bash
rtk pipeline explain --agent cursor --tool Shell --command "git status"

explain reports the subject your tool maps to, which rules fire, and how far RTK can take the output on that agent. Agent names accept either spelling — claude and claude-code both work.


Guide Resources Editions (`docs/guide/resources/editions.md`)


title: Editions and Licensing sidebar: order: 5

Editions and Licensing

RTK ships as three downloads. Which one you want depends on what you need and which licence your organisation accepts.

Download Binary Licence Contents
rtk-<target> rtk Apache-2.0 + BUSL-1.1 Hook core, every command filter, and the compression engine
rtk-oss-<target> rtk Apache-2.0 only Hook core and every command filter. No engine.
rtk-proxy-<target> rtk-proxy BUSL-1.1 The RTK Proxy daemon. Never bundled with either rtk.

The two rtk downloads install a binary with the same name, so hooks, scripts and agent configuration work unchanged across editions. They cannot be installed side by side.

Which one should I install?

Most people want the default rtk. It is the complete tool and free for production use — see the grant below.

Install the Apache-2.0 edition if your organisation requires that every binary it runs carry an OSI-approved licence, or if you intend to redistribute RTK as part of a commercial offering.

Add rtk-proxy only if you want the RTK Proxy. It is independent of both rtk editions.

What the Apache-2.0 edition leaves out

It is the same binary minus the compression engine. What that removes:

Absent What you lose
rtk pipeline Inspecting, validating, explaining and benchmarking pipelines
rtk methods Listing registered compression methods
rtk tasks Listing and running delegation tasks
PostToolUse compression Compressing the output of an agent's own Read and MCP tools
Pipeline files A pipeline.toml on disk is inert, exactly as in a build predating the engine

What stays is the part you use all day. The PreToolUse rewrite hook — the mechanism that turns your agent's git status into rtk git status — is Apache-2.0 and behaves identically. So does every command filter, rtk init, rtk gain, rtk discover, rtk session, rtk recall, and rtk llm-proxy.

The practical difference: bash output is filtered exactly as in the full edition; output your agent reads through its own file and MCP tools is not.

Installing the Apache-2.0 edition

Set RTK_EDITION=oss for the install script:

bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | RTK_EDITION=oss sh

Homebrew ships it as a separate formula:

bash
brew install rtk-ai/tap/rtk-oss

Debian and RPM packages are published as rtk-oss.

The two editions cannot coexist — both install /usr/bin/rtk. Homebrew declares this explicitly (conflicts_with), so brew refuses the second install with a clear message. The deb and rpm packages carry no declared Conflicts:, so dpkg/rpm stop on the file collision instead. Uninstall one edition before installing the other.

From a source checkout:

bash
cargo build --release --no-default-features

--no-default-features removes rtk-engine from the dependency graph entirely — no BUSL-licensed code is compiled into the binary.

Licence terms

  • Apache-2.0 covers the rtk CLI and its filters.
  • BUSL-1.1 covers the compression engine and the rtk-proxy daemon.

The BUSL Additional Use Grant permits free production use, including running the daemon. What it does not permit is offering RTK — or a derivative — to third parties as a commercial product or service.

For resale, embedding in a commercial offering, or any use the grant does not cover, contact [email protected].

Licence files travel with every download: the rtk archive carries LICENSE and LICENSE-ENGINE, the Apache-2.0 archive carries LICENSE alone, and the daemon archive carries its own LICENSE.


Guide Resources Savings Explained (`docs/guide/resources/savings-explained.md`)


title: How RTK Savings Work sidebar: order: 2

How RTK Savings Work

RTK cuts up to 90% of the bash output your agent reads. This page explains what that number measures, what it does not measure, and how it reaches your bill.

What RTK filters

RTK sits between your agent and the CLI. When the agent runs a shell command, RTK executes it, compresses the output, and returns the compressed version.

text
agent runs a shell command
        |
        v
   RTK filters the output
        |
        v
  agent reads the result

The only thing RTK changes is the bytes a shell command sends back. Everything RTK reports as "savings" is measured on those bytes.

The savings chain

text
Cost
├─ Input tokens
│  ├─ Bash output           <- the only part RTK filters
│  ├─ Your prompt
│  ├─ System prompt
│  └─ Conversation history
└─ Output tokens            <- what the model writes

Those bytes are one contributor to input tokens, alongside your prompt, the system prompt, and conversation history. Input tokens are in turn only part of the bill, which also counts output tokens.

So the reduction dilutes at every step: a large cut in bash output produces a smaller cut in input tokens, and a smaller one again in cost. A command showing 90% fewer output bytes does not make your session 90% cheaper.

This is why RTK reports bash output reduction rather than a cost figure. Bash output is the part RTK controls; the rest depends on your prompt, your model, how much the agent writes back, and how much of the conversation is replayed on each call.

Why the token counts are estimates

rtk gain estimates tokens as bytes / 4 (rounded up). RTK ships no tokenizer, so:

  • The percentage is reliable. The same estimator is applied to the raw output and the filtered output, so the ratio between them holds regardless of the estimator's absolute accuracy.
  • The absolute token counts are approximate. They will not match your provider's billing. Treat a figure such as Input tokens: 50,000 as an order of magnitude, not an invoice line.

If you need exact counts, run the raw and filtered output through your model's own tokenizer.

How to read `rtk gain`

Column What it actually is
Input Estimated tokens from raw command output, bytes / 4
Output Estimated tokens after filtering, bytes / 4
Saved Input minus Output, in estimated tokens
Save% Reduction in bash output bytes

Save% is the meaningful number. It is a byte ratio, and it is accurate as a ratio.

What RTK does not reduce

  • Output tokens. RTK never touches what the model writes.
  • Your prompt, the system prompt, or conversation history. These are input tokens RTK has no visibility into.
  • Commands with no matching filter. These pass through untouched and are tracked at 0% savings. See rtk gain --history.

See also


Guide Resources Telemetry (`docs/guide/resources/telemetry.md`)


title: Telemetry & Privacy sidebar: order: 4

Telemetry & Privacy

Telemetry is off by default. RTK sends nothing until you give explicit consent, either at the rtk init prompt ([y/N], default no) or with rtk telemetry enable. Once enabled, RTK sends anonymous, aggregate usage metrics at most once per day.

Data Collector

Entity: RTK AI Labs
Contact: [email protected]

How it works

  1. Once per day (23-hour interval), RTK sends a single HTTPS POST to the telemetry endpoint
  2. The ping runs in a background thread and never blocks the CLI (2-second timeout)
  3. A marker file prevents duplicate pings within the interval
  4. If the server is unreachable, the ping is silently dropped — no retries, no queue

What is collected

Example values below are illustrative.

Identity (anonymous)

Field Example Description
device_hash a3f8c9... (64 hex chars) SHA-256 of a per-device random salt stored locally (~/.local/share/rtk/.device_salt). Not reversible. No hostname or username included.

Environment

Field Example Description
version 1.0.0 RTK version
os macos Operating system
arch aarch64 CPU architecture
install_method homebrew How RTK was installed (homebrew/cargo/script/nix)

Usage volume

Field Example Description
commands_24h 100 Commands run in the last 24 hours
commands_total 10000 Commands run since install
top_commands ["git", "cargo", "ls"] Most used tools (names only, max 5)
tokens_saved_24h 100000 Estimated tokens saved in the last 24 hours
tokens_saved_total 10000000 Estimated tokens saved since install
savings_pct 70.0 Overall bash output reduction

Filter quality

Field Example Description
passthrough_top ["git:10", "npm:5"] Top 5 commands with 0% bash output reduction
parse_failures_24h 0 Filter parse failures in the last 24 hours
low_savings_commands ["rtk <cmd>:25%"] Commands with a weighted bash output reduction below 30% (net-negative ones included). The rate is SUM(saved)/SUM(input) over every call of the command, the same figure as the rtk gain By Command table. Exact 0% is reported in passthrough_top instead
avg_savings_per_command 70.0 Unweighted average of per-command reduction rates across distinct command names; commands that never had any input are skipped

Ecosystem distribution

Field Example Description
ecosystem_mix {"git": 50, "cargo": 20, "js": 10} Share of commands per tool category, in percent

Activity

Field Example Description
first_seen_days 30 Installation age in days
active_days_30d 20 Days with at least 1 command in the last 30 days

Economics

Field Example Description
tokens_saved_30d 1000000 Estimated tokens saved in the last 30 days
estimated_savings_usd_30d — A USD value derived from the estimated tokens saved and a fixed per-token rate. It is not a measured cost and does not reflect any provider's pricing

Integration

Field Example Description
hook_type claude Which AI agent hook is installed (claude/gemini/codex/cursor/none)
custom_toml_filters 1 Number of user-created TOML filter files

Configuration

Field Example Description
has_config_toml true Whether a config.toml exists
exclude_commands_count 2 Number of entries in hooks.exclude_commands
projects_count 5 Number of distinct project paths

Feature usage

Field Example Description
meta_usage {"gain": 5, "discover": 2} How often RTK's own commands are run

What is NOT collected

  • Source code or file contents
  • Full command lines or arguments (only tool names like "git", "cargo")
  • File paths or directory structures
  • Secrets, API keys, or environment variable values
  • Repository names or URLs
  • Personally identifiable information
  • IP addresses (not stored in telemetry pings; stored temporarily in erasure audit log for accountability, anonymized after 6 months)

Telemetry requires explicit opt-in consent (GDPR Art. 6, 7). Consent is requested during rtk init or via rtk telemetry enable. Without consent, no data is sent.

bash
rtk telemetry status     # Check current consent state
rtk telemetry enable     # Give consent (interactive prompt)
rtk telemetry disable    # Withdraw consent
rtk telemetry forget     # Withdraw consent + delete local data + request server erasure

Environment variable override (blocks telemetry regardless of consent):

bash
export RTK_TELEMETRY_DISABLED=1

Retention Policy

  • Server-side: telemetry records are retained for a maximum of 12 months, then automatically purged.
  • Server-side (erasure log): IP addresses in the erasure audit log are anonymized after 6 months (GDPR — IP is personal data).
  • Client-side: the local SQLite database (~/.local/share/rtk/history.db) retains data for 90 days by default (configurable via tracking.history_days in config.toml). Deleted entirely by rtk telemetry forget.

Your Rights (GDPR)

Under the EU General Data Protection Regulation, you have the right to:

  • Access your data: rtk telemetry status shows your device hash; the telemetry payload is fully documented above.
  • Rectification: since data is anonymous and aggregate, rectification is not applicable.
  • Erasure (Art. 17): run rtk telemetry forget to delete local data and send an erasure request to the server. Alternatively, email [email protected] with your device hash.
  • Restriction of processing: rtk telemetry disable stops all data collection immediately.
  • Portability: the local SQLite database at ~/.local/share/rtk/history.db contains all locally stored data.
  • Objection: rtk telemetry disable or export RTK_TELEMETRY_DISABLED=1.

Erasure Procedure

  1. Run rtk telemetry forget — this disables telemetry, deletes your device salt, ping marker, and local tracking database (history.db), then sends an erasure request to the server.
  2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email [email protected] for manual erasure.
  3. You can also email [email protected] directly to request manual erasure.

Data Handling

  • All communications use HTTPS (TLS)
  • Data is used exclusively for RTK product improvement
  • No data is sold or shared with third parties
  • Aggregate statistics may be published

Guide Resources Troubleshooting (`docs/guide/resources/troubleshooting.md`)


title: Troubleshooting sidebar: order: 3

Troubleshooting

`rtk gain` says "not a rtk command"

Symptom:

bash
$ rtk gain
rtk: 'gain' is not a rtk command. See 'rtk --help'.

Cause: You installed Rust Type Kit (reachingforthejack/rtk) instead of Rust Token Killer (rtk-ai/rtk). They share the same binary name.

Fix:

bash
cargo uninstall rtk
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
rtk gain    # should now show token savings stats

How to tell which rtk you have

If rtk gain... You have
Shows token savings dashboard Rust Token Killer ✅
Returns "not a rtk command" Rust Type Kit ❌

AI assistant not using RTK

Symptom: Claude Code (or another agent) runs cargo test instead of rtk cargo test.

Checklist:

  1. Verify RTK is installed:

    bash
    rtk --version
    rtk gain
  2. Initialize the hook:

    bash
    rtk init --global    # Claude Code
    rtk init --global --agent cursor    # Cursor
    rtk init --global --opencode  # OpenCode
  3. Restart your AI assistant.

  4. Verify hook status:

    bash
    rtk init --show
  5. Check settings.json has the hook registered (Claude Code):

    bash
    cat ~/.claude/settings.json | grep rtk

RTK not found after `cargo install`

Symptom:

bash
$ rtk --version
zsh: command not found: rtk

Cause: ~/.cargo/bin is not in your PATH.

Fix:

For bash (~/.bashrc) or zsh (~/.zshrc):

bash
export PATH="$HOME/.cargo/bin:$PATH"

For fish (~/.config/fish/config.fish):

fish
set -gx PATH $HOME/.cargo/bin $PATH

Then reload:

bash
source ~/.zshrc    # or ~/.bashrc
rtk --version

RTK on Windows

Double-clicking rtk.exe does nothing

Symptom: You double-click rtk.exe, a terminal flashes and closes instantly.

Cause: RTK is a command-line tool. With no arguments, it prints usage and exits. The console window opens and closes before you can read anything.

Fix: Open a terminal first, then run RTK from there:

  • Press Win+R, type cmd, press Enter
  • Or open PowerShell or Windows Terminal
  • Then run: rtk --version

Hook not working (no auto-rewrite)

Symptom: On native Windows, commands are not auto-rewritten. An older rtk init -g printed "Falling back to --claude-md mode".

Cause: Before v0.37.2, rtk init -g registered no hook on native Windows: it fell back to injecting the full RTK instructions into ~/.claude/CLAUDE.md. A setup made then still has no hook.

Fix: Upgrade to v0.37.2 or later, where rtk init -g registers the auto-rewrite hook on Windows as a native binary command (rtk hook claude). No Unix shell, bash, or jq is required. Re-run rtk init -g: it replaces the CLAUDE.md block with an @RTK.md reference and, once you confirm, adds rtk hook claude to settings.json. A legacy ~/.claude/hooks/rtk-rewrite.sh hook, if one exists, is deleted along with its .rtk-hook.sha256 and its settings.json entry.

Answer y when it asks to patch settings.json. Outside a terminal it cannot ask and defaults to N, so use --auto-patch there:

powershell
rtk init -g                # answer y at the settings.json prompt
rtk init -g --auto-patch   # or: patch settings.json without asking

The default N leaves no hook registered: any legacy rtk-rewrite.sh entry is removed and rtk hook claude is not added. The RTK hook registered (global). banner prints either way; the line that confirms the patch is settings.json: hook added (or settings.json: hook already present when an earlier run added it). Restart Claude Code, then confirm:

powershell
rtk init --show

It should report [ok] Hook: rtk hook claude (native binary command).

WSL also works and behaves like Linux if you prefer it.

Node.js tools not found

Symptom:

text
rtk vitest --run
Error: program not found

Cause: On Windows, Node.js tools are installed as .CMD/.BAT wrappers. Older RTK versions couldn't find them.

Fix: Update to RTK v0.23.1+:

bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
rtk --version    # should be 0.23.1+

Compilation error during installation

bash
rustup update stable
rustup default stable
cargo clean
cargo build --release
cargo install --path . --force

Minimum required Rust version: 1.91 (edition 2024 needs Cargo 1.85 or newer).

OpenCode not using RTK

bash
rtk init --global --opencode
# restart OpenCode
rtk init --show    # should show "OpenCode: plugin installed"

The plugin needs OpenCode 1.18.29 or newer. With an older version, rtk init
installs nothing and prints the version it found, when it can read
opencode --version. Upgrade OpenCode rather than looking for a rewrite
setting.

If the plugin loads but nothing changes, check that rtk is on the PATH
OpenCode itself sees. The plugin only resolves it from PATH, because
OpenCode's shell tool does not source .profile, .bashrc or .bash_profile
and a binary found elsewhere cannot be spawned as a bare rtk. Set
RTK_DISABLED=1 to confirm which way round the behaviour goes.

`cargo install rtk` installs the wrong package

If Rust Type Kit is published to crates.io under the name rtk, cargo install rtk may install the wrong one.

Always use the explicit URL, pinned to the release branch:

bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk

Does RTK break Claude's prompt cache?

No. RTK filters command output once, at execution time. The filtered result is written into the
conversation history and never changes afterwards, and prompt caching matches on a stable prefix
— RTK does not rewrite anything the cache has already seen.

Smaller tool results also make caching cheaper: cache writes bill at 1.25x and cache reads at
0.1x the input rate, so fewer tokens in means less to write once and less to re-read every turn.

To see your own cache write and read volumes next to RTK's savings:

bash
rtk cc-economics

rtk gain reports token savings only; the cache breakdown lives in rtk cc-economics.

Run the diagnostic script

From the RTK repository root:

bash
bash scripts/check-installation.sh

Checks:

  • RTK installed and in PATH
  • Correct version (Token Killer, not Type Kit)
  • Available features
  • Claude Code integration
  • Hook status

Still stuck?

Open an issue: https://github.com/rtk-ai/rtk/issues


Guide Resources What Rtk Covers (`docs/guide/resources/what-rtk-covers.md`)


title: What RTK Optimizes sidebar: order: 1

What RTK Optimizes

Once RTK is installed with a hook, these commands are automatically intercepted and filtered. You run them normally — the hook rewrites them transparently before execution.

:::note
RTK reduces bash output bytes — the only thing it controls. Those bytes are one contributor to input tokens, and input tokens are only part of a bill that also counts output tokens. See How RTK Savings Work.
:::

Git

Command What changes
git status Compact stat format, grouped by state
git log Hash + author + subject only
git diff Context reduced, headers stripped
git show Same as diff
git stash list Compact one-line per entry

GitHub CLI

Command What changes
gh pr view Removes ASCII art and verbose metadata
gh pr checks Status + name only, failures highlighted
gh run list Compact workflow run summary
gh issue view Body only, no decoration

Graphite (Stacked PRs)

Command What changes
gt log Stack summary only
gt status Current branch context

Cargo / Rust

Command What changes
cargo test Failures only, passed tests suppressed
cargo build Errors and warnings only
cargo check Errors and warnings only
cargo clippy Lint warnings grouped by file

JavaScript / TypeScript

Command What changes
jest Failures only
vitest Failures only
tsc Type errors grouped by file
eslint Violations grouped by rule
pnpm list Compact dependency tree
pnpm outdated Package + current + latest only
next build Route summary + errors only
prisma migrate Migration status only
playwright test Failures + trace links only

Python

Command What changes
pytest Failures only
ruff check Violations grouped by file
mypy Type errors grouped by file
pip install Installed packages only, progress stripped

Go

Command What changes
go test Failures only
golangci-lint run Violations grouped by file
go build Errors only

Ruby

Command What changes
rspec Failures only
rubocop Offenses grouped by file
rake Task output, build errors highlighted

.NET

Command What changes
dotnet build Errors and warnings only
dotnet restore Restore summary, errors and warnings kept

Docker / Kubernetes

Command What changes
docker ps Essential columns (name, image, status, port)
docker images Name + tag + size only
docker logs Deduplicated, last N lines
kubectl get pods Name + status + restarts only
kubectl logs Deduplicated entries
Command What changes
ls Tree format with file counts
find Tree format
grep Truncated lines, grouped by file
ast-grep Grouped by file, capped overflow; --json passes through
diff Context reduced
wc Compact counts
cat / head / tail <file> Smart file reading via rtk read
rtk smart <file> 2-line heuristic code summary (signatures only)

Cloud and Data

Command What changes
aws JSON condensed, relevant fields only
psql Query results without decoration
curl Response body only, headers stripped

Global flags

These flags apply to all RTK commands and must come before the subcommand:

Flag Description
-v / --verbose Show filtering details on stderr (-v, -vv, -vvv for increasing detail)
bash
# Debug: see what RTK is doing
rtk -vvv git status

Commands that are not rewritten

If a command isn't in the list above, RTK runs it through passthrough — the output reaches the LLM unchanged. You can explicitly track unsupported commands:

bash
rtk bypass make install    # runs make install, tracks usage, no filtering

To check which commands were missed opportunities: rtk discover.


--- METRICS ---