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
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 | shThen check you have the right package — another project named "rtk" exists on crates.io:
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.
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].
curl -fsSL .../install.sh | RTK_EDITION=oss sh # Apache-2.0 edition
brew install rtk-ai/tap/rtk-ossWhat the Apache-2.0 edition omits and where the licence boundary falls: Editions and Licensing
Quick Start
# 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 statusThe 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
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:
- Smart Filtering - Removes noise (comments, whitespace, boilerplate)
- Grouping - Aggregates similar items (files by directory, errors by type)
- Truncation - Keeps relevant context, cuts redundancy
- 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.
-v, --verbose # increase verbosity (-v, -vv, -vvv)
rtk bypass <cmd> # run one command unfiltered, still trackedEvery command and its filter: What RTK Optimizes
Measure It
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 sessionsAttribution, 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).
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 sideTwo 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:
# ~/.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
- Patrick Szymkowiak — Founder
GitHub · LinkedIn - Florian Bruniaux — Core contributor
GitHub · LinkedIn - Adrien Eppling — Core contributor
GitHub · LinkedIn - Nicolas Le Cam — Core contributor
Github · LinkedIn - Takayuki Maeda — Core contributor
GitHub · LinkedIn
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 getrtk-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
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 LLMAll 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,PreToolUseJSON format,settings.jsonpatching, test scriptcopilot/— Rust binary hook, dual format (VS Code Chat vs Copilot CLI), deny-with-suggestion fallbackcursor/— Shell hook, Cursor JSON format, empty{}response requirementcline/— Rules file (prompt-level),.clinerulesproject-local installationwindsurf/— Rules file (prompt-level),.windsurfrulesworkspace-scopedcodex/— Rust binary hook (rtk hook codex),PreToolUse.updatedInput,.codex/hooks.json/$CODEX_HOME/hooks.json, plusAGENTS.mdawarenessopencode/— TypeScript plugin, OpenCode 2.0 and 1.x >= 1.18.29, in-place mutationpi/— TypeScript extension,tool_callevent, localisBashToolCallEventguard, in-place mutation,~/.pi/agent/extensions/; shared with Oh My Pi (OMP) — OMP installs the same file at.omp/extensions/via itslegacy-pi-compatlayerhermes/— Python plugin,pre_tool_callhook, in-place terminal command mutationvibe/— Rust binary hook (rtk hook vibe),pre_toolentry in~/.vibe/hooks.toml,hook_specific_output.tool_inputrewrite plussystem_messagefor UI visibilityantigravity/— Rust binary hook (rtk hook antigravity),PreToolUseplugin entry in.agents/plugins/rtk/hooks.jsonor~/.gemini/config/plugins/rtk/hooks.json, transparent rewrite viaoverwrite.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):
{
"tool_name": "Bash",
"tool_input": { "command": "git status" }
}Output (stdout, when rewritten):
{
"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):
{
"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:
{
"tool_name": "RunCommand",
"tool_input": { "command": "git status", "description": "Check status" }
}Output (only when rewritten):
{
"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):
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"permission_mode": "default",
"tool_input": { "command": "git status" }
}Output (stdout, when rewritten):
{
"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):
{
"toolName": "bash",
"toolArgs": "{\"command\": \"git status\"}"
}Output (no updatedInput support -- uses deny-with-suggestion):
{
"permissionDecision": "deny",
"permissionDecisionReason": "Token savings: use `rtk git status` instead"
}VS Code Copilot Chat (Rust Binary)
Input (stdin, snake_case):
{
"tool_name": "Bash",
"tool_input": { "command": "git status" }
}Output: Same as Claude Code format (with updatedInput).
Gemini CLI (Rust Binary)
Input (stdin):
{
"tool_name": "run_shell_command",
"tool_input": { "command": "git status" }
}Output (when rewritten):
{
"decision": "allow",
"hookSpecificOutput": {
"tool_input": { "command": "rtk git status" }
}
}No rewrite: {"decision": "allow"}
Mistral Vibe (Rust Binary)
Input (stdin):
{
"tool_name": "bash",
"tool_input": { "command": "git status" },
"hook_event_name": "pre_tool",
"session_id": "..."
}Output (when rewritten):
{
"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):
{
"toolCall": {
"name": "run_command",
"args": { "CommandLine": "git status" }
}
}Output (when rewritten):
{
"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:
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:
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"] = rewrittenCommand 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, andwcinvocations; 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 statusruns 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"excludesgit push origin main). Patterns starting with^are treated as regex.- Already-RTK:
rtk git statuspasses through unchanged (nortk 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 rewritecrashes: 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:
brew install rtk
# or
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | shInstall the plugin
# 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 restartOr install via OpenClaw CLI
openclaw plugins install ./openclawConfiguration
In openclaw.json:
{
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.denyrule 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 stopgit -C . push— so a deny rule is not a security boundary. - A command matching a
permissions.askrule 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 thertk CLI. It does four things: intercept, compress, forward, record.
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 gainUser 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 |
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 sideBuilding
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.
cargo build --release --manifest-path proxy/Cargo.toml # the daemon
cargo test --manifest-path proxy/Cargo.toml # unit + functional + real-binary smokeContributing 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 onrtk: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
PreToolUselifecycle hook (rtk hook antigravity) providing transparent command rewriting viaoverwrite.CommandLine - Supported across all Antigravity surfaces: Antigravity CLI (
agy), Antigravity IDE, and Antigravity 2.0 - Installs the awareness file selected by
awareness.levelasrules/AGENTS.mdinside 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/
- Local workspace:
- Full lifecycle support:
- Setup:
rtk init --agent antigravity(orrtk init -g --agent antigravity) - Preview:
rtk init --agent antigravity --dry-run - Uninstallation:
rtk init --agent antigravity --uninstall(orrtk init -g --agent antigravity --uninstall)
- Setup:
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
PreToolUsehook -- requiresjqfor JSON parsing - Returns
updatedInputJSON 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 --versionagainst minimum 0.23.0 ../rtk-awareness.md(or-high/-full, perawareness.levelin config.toml) is the instructions file embedded into CLAUDE.md byrtk 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 withrtk, plus meta commands. No hook, so thefulllevel is always used regardless ofawareness.level - Installed to
.clinerules(project-local) byrtk init
Overview (`hooks/codex/README.md`)
Codex CLI Hooks
Part of
hooks/
Specifics
- Native Rust
PreToolUseprocessor:rtk hook codex - Transparently rewrites
tool_input.commandwith Codex'supdatedInputresponse - Registers a
Bashmatcher in.codex/hooks.json(project) or$CODEX_HOME/hooks.json(global) - Writes the shared awareness document selected by
awareness.leveltoRTK.md, referenced fromAGENTS.md - Installed by
rtk init --codex(project) orrtk init -g --codex(global) - Uninstalled by adding
--uninstallto 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, runrtk init --codex outside the sandbox to see an optional Codex configuration
snippet using the actual database parent directory. The path followsRTK_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 copilotRust binary (not a shell script) -- nojqdependency - Auto-detects two input formats: VS Code Copilot Chat (snake_case
tool_name/tool_input) and Copilot CLI (camelCasetoolName/toolArgswith JSON-stringified args) - VS Code format: returns
updatedInputfor transparent rewrite - Copilot CLI format: returns
permissionDecision: "deny"with suggestion (Copilot CLI API doesn't supportupdatedInput)
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_inputinstead ofhookSpecificOutput/updatedInput) - Returns
{}(empty JSON) when no rewrite applies -- Cursor requires JSON output for all code paths - Requires
jqandrtk >= 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
rtk init --agent hermesThe 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.
rtkis missing fromPATHrtk rewriteexits 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
terminaltool calls are rewritten. - Commands skipped by
rtk rewritestay unchanged, including commands already prefixed withrtk, 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 withrtk, plus meta commands. No hook, so thefulllevel is always used regardless ofawareness.level - Installed to
.kilocode/rules/rtk-rules.md(project-local) byrtk 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.beforevia{ 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 --opencodewrites nothing and leaves an existing plugin alone, when it can readopencode --version - Thin delegating shim: calls
rtk hook opencodeas 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 projectopencode.json/.jsoncin the order OpenCode resolves them: the legacypermissionmap first, then the 2.xpermissionslist, whatever order the file declares them. Every file's root rules load before any file's agent rules, so a projectallowcannot outrank a global agentask. 2.x list actions match as globs againstshell, sosh*covers it; a list rule whose action only namesbashdoes not count, because OpenCode ignores it there. A legacy map key the two generations read differently (shellandsh*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 inagents/oragent/, under~/.config/opencode/or any.opencode/from the working directory up, whose frontmatter declarespermission:orpermissions:, 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
PATHonly (PATHEXTon Windows), and skips a PATH entry that is a directory. OpenCode's shell tool runs with OpenCode's ownPATHand never sources.profile/.bashrc, so an rtk found outsidePATHcannot be spawned as a barertk …— 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 reportsrtk 0.49.0and so does a release that lacks the subcommand - Passes OpenCode 2.x's
event.agentasrtk hook opencode --agent <name>, so anagent.<name>.permissionask 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 isRTK_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 readsRTK_DISABLED=1from OpenCode's own process environment, which switches rewriting off for the whole session - Mutates command in-place (
event.input.commandin v2,output.args.commandin 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.tsbyrtk 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 theirrtk-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 gatesrm -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, nozxdependency) - Subscribes to
tool_callevent, narrows tobashtool - Calls
rtk rewriteviapi.exec; mutatesevent.input.commandin-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-patchapproves and--no-patchleaves the file unchanged. A declined protected update, including--no-patch, exits nonzero; in--dry-runmode 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.tsbyrtk init --agent pi(project-local) or~/.pi/agent/extensions/rtk.tsbyrtk 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
# 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.tsUninstalling 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 vibeRust binary (not a shell script) -- nojqdependency pre_toolhook declared in~/.vibe/hooks.toml(user-scoped) withmatch = "bash"andstrict = false- Reads Vibe's stdin JSON payload (
tool_name,tool_input.command,hook_event_name,session_id) - Returns
hook_specific_output.tool_input.commandfor transparent rewrite plus asystem_messagefor 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.mdis installed as a belt-and-suspenders fallback (skip with--hook-only); its content followsawareness.levelin config.toml (../rtk-awareness*.md) - Installed globally via
rtk init -g --agent vibe; there is no project-scoped variant
Notes
- The hook is a subcommand of the RTK binary (
rtk hook vibe); no separate script or plugin file is installed - Vibe hook contract reference: https://docs.mistral.ai/vibe/code/cli/hooks
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 withrtk, plus meta commands. No hook, so thefulllevel is always used regardless ofawareness.level - Installed to
.windsurfrules(project-local, workspace-scoped) byrtk 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_argparsing, gh markdown filtering, gt passthroughrust/— cargo, runner (err/test) — Cargo sub-enum routing, runner dual-modejs/— npm, pnpm, vitest, lint, tsc, next, prettier, playwright, prisma — Package manager auto-detection, lint routing, cross-deps with pythonpython/— ruff, pytest, mypy, pip — JSON check vs text format, state machine parsing, uv auto-detectiongo/— go test/build/vet, golangci-lint — NDJSON streaming, Go sub-enum patterndotnet/— dotnet, binlog, trx, format_report — DotnetCommands sub-enum, internal helper modulescloud/— aws, docker/kubectl, curl, wget, psql — Docker/Kubectl sub-enums, JSON forced outputsystem/— ls, tree, read, grep, find, wc, env, json, log, deps, summary, format, smart — format_cmd routing, filter levels, language detectionruby/— 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.
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
- Spawn —
run_streaming()starts the child process with piped stdout/stderr (or inherited TTY for Passthrough) - Filter — stdout is processed per the FilterMode; stderr is forwarded to the terminal in real time via a dedicated reader thread
- Print — filtered output is written to stdout (live for Streaming, post-hoc for CaptureOnly/Buffered); if tee enabled, appends recovery hint on failure
- Track —
timer.track()records raw vs filtered for token savings - Exit code — returns
Ok(exit_code)to caller;main.rscallsprocess::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):
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.
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.
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.
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):
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
runner::run_passthrough("mycmd", args, verbose)
}Example — manual execution (custom logic):
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_cmdroutes tomypy_cmdorruff_cmdwhen detecting Python projectsformat_cmdroutes toprettier_cmdorruff_cmddepending on the formatter detectedgh_cmdimportscompact_diff()fromgitfor diff formatting (markdown helpers are defined ingh_cmditself)
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)
- Create module in
src/cmds/<ecosystem>/mycmd_cmd.rs:- Write the
filter_mycmd()function (pure:&str -> String, no side effects) - Write
pub fn run(...) -> Resultusingrunner::run_filtered()— build theCommand, chooseRunOptions, 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) orforce_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 thetakes_valuepredicate from the tool's own--helpand check it against the real binary
- Write the
- Register module:
- Add
pub mod mycmd_cmd;to the ecosystemmod.rs, keeping the list alphabetical. Modules are listed explicitly rather than generated from the directory: rustfmt only follows literalmoditems and cannot expand macros, so a generated list hides every file undersrc/cmds/fromcargo fmt— and therefore from CI's fmt gate.build.rsfails the build if a.rsfile here is missing from itsmod.rs, since an undeclared module is never compiled, linted, or tested. - Add variant to
Commandsenum inmain.rswith#[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)?,
- Add
- Add rewrite pattern — Entry in
src/discover/rules.rs(PATTERNS + RULES arrays at matching index) so hooks auto-rewrite the command - Write tests — Real fixture, snapshot test, >= 20% reduction in bash output (measured with RTK's token estimator, see testing rules)
- Update docs — Ecosystem README (CHANGELOG.md is auto-generated by release-please)
TOML filter (simple line-based filtering)
- Create filter in
src/filters/ - Add rewrite pattern in
src/discover/rules.rs - 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:
- strip_ansi: Remove ANSI escape codes if enabled
- replace: Line-by-line regex substitutions (chainable, supports backreferences)
- match_output: Short-circuit rules (if output matches pattern, return message;
unlessfield prevents swallowing errors) - strip/keep_lines: Filter lines by regex (mutually exclusive)
- truncate_lines_at: Truncate each line to N chars (unicode-safe)
- head/tail_lines: Keep first N or last N lines (with omit message)
- max_lines: Absolute line cap applied after head/tail
- on_empty: Return message if result is empty after all stages
Three-tier filter lookup (first match wins):
.rtk/filters.toml(project-local, requiresrtk trust)~/.config/rtk/filters.toml(user-global)- Built-in filters concatenated by
build.rsat 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 inLanguage::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:
QUERY = """ # contains """ without starting with it
SELECT 1
"""
"""Module doc.""" # opens and closes on one lineDocstrings 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
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
[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 = 2000Shared 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 —
-umeans-pingit logbut--include-untrackedingit stash show, and-Tis--initial-tabin grep but--type-notin rg. Transcribe from the tool's own--helpand 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_dashdashgives the tool's own tokens.Dialect::Msbuildkeeps 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-changessilently 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-compactand 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=vor--flag v, and a literal--is the boundary. The common case.ValueSpec::attached_only()—--flag=vonly, the next argument is never the value (git's-M/-U/-C/-Btake an optional attached number and nothing else).ValueSpec::solo_only()— aShortflag takes a separate value only when it is the whole argument:git log -n 2does,git log -pn 2does not. No meaning for aLongflag..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 withAtomic: the library's short options are whole multi-character words (-pl,-am,-gs,-emp), somvn -Bois an error, not a cluster. Maven is the first consumer.Dialect::GoFlag— Go'sflagpackage: atomic single-dash options, and-runis 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:
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.Analyze history —
rtk discoverscans 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".
"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:
- Strip trailing redirects (
2>&1,>/dev/null) — matched via lexer tokens, set aside, re-appended after rewriting - 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 producertk read -20 file(wrong flag position) - 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 fromrules.rs - 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=1in the env prefix → skip rewriteghwith--json/--jq/--template→ skip (structured output, rtk would corrupt it)catwith flags other than-n→ skip (different semantics thanrtk read)cat/head/tailwith>or>>→ skip (write operation, not a read)- Command in
hooks.exclude_commandsconfig → 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:
- Extracts commands from the JSONL (via
SessionProvidertrait — currently only Claude Code) - Splits compound commands using the same lexer-based tokenization
- Classifies each command against the same rules used for live rewriting
- 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 byRegexSetfor 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 reportssubcmd_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 installedlines - 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
- Copy any existing
.tomlfile and rename it (e.g.my-tool.toml) - Update the three required fields:
description,match_command, and at least one action field - Add
[[tests.my-tool]]entries to verify the filter behaves correctly - Run
cargo test— the build step validates TOML syntax and runs inline tests
File format
[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.
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 & RFilter lookup priority
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:
[rtk] warning: filter 'make' is shadowing a built-in filterCustom 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:
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 initdetects existing custom filters and lets you enable them — an interactive[y/N], or--trust-filters/--no-trust-filtersfor 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.rsis 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-processrtk hook <agent>hosts (hook_cmd.rs), thertk rewritesubprocess path (rewrite_cmd.rs), and thertk hook checkdiagnostic (main.rs). Add a gate there, not in a caller.rewrite_cmd.rsis a thin CLI bridge — it exists to serve hooks (hooks callrtk rewriteas a subprocess) and rendersdecision.rs's verdict as the exit codes those delegates branch on.trust.rsgates 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:
- At install:
integrity::store_hash()computes SHA-256 of the hook file, writes to~/.claude/hooks/.rtk-hook.sha256(read-only 0o444) - At runtime:
integrity::runtime_check()re-computes hash and compares; blocks execution if tampered - On demand:
rtk verifyprints 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:
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 configuredtransparent_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'sApprovalOwner.
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, returnsPermissionVerdictrewrite_cmd.rs— maps verdict to exit code (consumed by shell hook)hook_cmd.rs— maps decisions to each agent's JSON protocol, including CodexupdatedInputdiscover/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 scoreCorrectionRule— 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(routesrtk learncommand)
Detection Algorithm
- Extract all commands from JSONL sessions via
ClaudeProvider - Scan chronologically for fail-then-succeed pairs (same base command, first has error output, second succeeds)
- Classify the error type using regex patterns on the error output
- Assign confidence scores based on similarity and error clarity
- Deduplicate into rules (merge identical wrong->right patterns, count occurrences)
- Filter by
--min-confidenceand--min-occurrencesthresholds
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
┌─────────────────────────────────────────────────────────┐
│ 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
- Implement
OutputParserfor a tool — try JSON (Tier 1), fall back to regex (Tier 2), then passthrough (Tier 3) - In command module: call
Parser::parse(), thendata.format(FormatMode::from_verbosity(verbose)) - 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 debuggingPatternMismatch: Regex pattern failedPartialParse: Some fields missingInvalidFormat: Unexpected structureMissingField: Required field absentVersionMismatch: Tool version incompatibleEmptyOutput: No data to parse
Degradation Warnings
[RTK:DEGRADED] vitest parser: JSON parse failed at line 42, using regex fallback
[RTK:PASSTHROUGH] playwright parser: Pattern mismatch, showing truncated outputMigration 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
- Maintenance: Tool version changes break gracefully (Tier 2/3 fallback)
- Reliability: Never silent failures or false data
- Observability: Clear degradation markers in verbose mode
- Token Efficiency: Structured data enables better compression
- Consistency: Unified interface across all tool types
- 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-healthcommand - 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 jsonfor structured parsing, usesforce_tee_hint()for truncation recovery, strips Lambda secrets. Shared runnerrun_aws_filtered()handles boilerplate for JSON-based filters; text-based filters (S3 ls, S3 sync/cp) have dedicated runnerscontainer.rshandles Docker, Kubernetes, and OpenShift;DockerCommands,KubectlCommands, andOcCommandssub-enums inmain.rsroute tocontainer::run()-- uses passthrough for unknown subcommandscurl_cmd.rstruncates long responses, saves full output to file for recoverywget_cmd.rswraps wget with output filteringpsql_cmd.rsfilters PostgreSQL query output
Overview (`src/cmds/dotnet/README.md`)
.NET Ecosystem
Part of
src/cmds/— see also docs/contributing/TECHNICAL.md
Specifics
dotnet_cmd.rsusesDotnetCommandssub-enum in main.rs- Internal helper modules (
dotnet_trx.rs,dotnet_format_report.rs,binlog.rs) are only used bydotnet_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 = trueso native git flags (--oneline,--cached, etc.) pass through correctly - Flag grammar (which flags take a value, what
-u/-pmean) is per-subcommand, not shared wholesale betweenlog/diff/show/stash show— seesrc/core/README.md - Default
git statususes--porcelain -bso the compact output never exceeds rawgit 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/--repoand-g/--groupat 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()redirectsmr/issue viewto passthrough when--webor--commentsis set- JSON handlers use the local
run_glab_json<F>()helper wrappingrunner::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 statususes text-keyword parsing (glab doesn't support-F jsonfor this subcommand); when no English status keyword is recognized (non-English locale), returns raw verbatimci traceuses ANSI-stripping + GitLab section-marker filtering + runner/git/artifact boilerplate removal; kept as text-only filter, not JSONrelease listfalls 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 matchgh_cmd.rsand avoid multi-byte rendering quirks
Cross-command
gh_cmd.rsimportscompact_diff()fromgit_cmd.rsfor diff formatting; markdown helpers (filter_markdown_body,filter_markdown_segment) are defined ingh_cmd.rsitselfglab_cmd.rsalso usescompact_diff()fromgit_cmd.rsformr diff; itsfilter_markdown_bodyis currently duplicated fromgh_cmd.rs(shared-module refactor deferred)diff_cmd.rsis a standalone ultra-condensed diff (separate fromgit 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.rsusesGoCommandssub-enum in main.rs (same pattern as git/cargo)go testoutputs NDJSON (-jsonflag injected by RTK) -- parsed line-by-line as streaming eventsgolangci_cmd.rsforces--out-format=jsonfor 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 managerlint_cmd.rsis a cross-ecosystem router: detects Python projects and delegates tomypy_cmdorruff_cmdvitest_cmd.rsuses theparser/module for structured output parsingplaywright_cmd.rsuses theparser/module for test result extraction
Cross-command
lint_cmdroutes tocmds/python/mypy_cmdandcmds/python/ruff_cmdfor Python projectsprettier_cmdis also called bycmds/system/format_cmdas 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 SUCCESSnorBUILD FAILUREappears as a trimmed line suffix, return the ANSI-stripped raw input unchanged. Protects non-English locales.Verbose bypass —
-X,--debug,-e,--errorsskip 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 whenF > 0orE > 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 anERROR!marker defensively (Surefire 3.5.5 emitsFAILURE!even for errors-only classes — failure detection keys off theFailures/Errorscounts, 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 (sharedBOILER_PREFIXES). Deliberately kept as signal:Failed to execute goalitself 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) andmvnw.cmd(Windows) detected via string-literalCommand::new(semgrep-safe); falls back toresolved_command("mvn").Maven Daemon (
rtk mvnd) —mvndis a separate entry point (run_daemon), not amvnalias: it shares phase detection and every filter, but always executesmvndand is never substituted by a./mvnwwrapper 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 usesis_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 —
mvndbuilds multi-module reactors in parallel by default and prefixes per-module lines with[module], interleaved line-by-line.split_laneclassifies on the prefix-stripped view while emitting the original (module identity survives), and each module gets its ownSurefireLane(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, javacsymbol:/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 gettingMAX_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 — butfilter_quiethas no keep-list, only a deliberate "keep anything else" safety net, sortk mvnd -qpasses that chatter through wherertk mvn -qemits 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 forheader and cleared onBUILD SUCCESS/BUILD FAILURE).Failure cap — both the count of emitted failing test classes and the size of the
[ERROR] Failures:summary block are bounded byMAX_MVN_FAILING_CLASSES = CAP_WARNINGS(the shared test-failure cap class fromsrc/core/truncate.rs, same binding as pytest/rspec/rake/runner). Excess emissions are replaced by a single… +N more failing test classes/… +N more failurestail (canonicaljoin_with_overflowshape) to keep large failure sets compact; the raw output stays recoverable via the tee[full output: …]hint. Per the core cap policy, a cap of0means 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.rssummarizesphp -lsyntax checks and routesphp artisan*to specialized helpersartisan_cmd.rscleans Artisan output and applies runner-aware filtering forphp artisan testphpunit_cmd.rsstrips progress/header noise and keeps failure details + final summaryphpstan_cmd.rsinjects JSON output by default and emits compact file/line error summariesecs_cmd.rscondenses EasyCodingStandard output while preserving file paths and error linespest_cmd.rsruns Pest with compact progress suppression and test-focused outputparatest_cmd.rsruns ParaTest with compact progress suppression and test-focused outputtest_output.rsprovides shared PHPUnit/Pest/ParaTest output filtering logicutils.rsresolves localvendor/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.rsuses a state machine text parser (no JSON available from pytest)ruff_cmd.rsuses JSON for check mode (--output-format=json) and text filtering for format modesqlfluff_cmd.rsinjects--format jsonfor 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.rsauto-detectsuvas a pip alternative and routes accordinglyuv_cmd.rspreservesuv runenvironment semantics: the program's own output passes through on success (bounded, with a tee hint), diagnostics are extracted on failurepython -m pytestandpython3 -m mypyare rewritten by the hook registry tortk pytest/rtk mypy
Cross-command
ruff_cmdis called bycmds/js/lint_cmdandcmds/system/format_cmdfor Python projectsmypy_cmdis called bycmds/js/lint_cmdwhen 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.rsfilters Minitest output viarake test/rails test; state machine text parser, failures only (85-90% reduction)rspec_cmd.rsuses JSON injection (--format json) with text fallback; failures only (60%+ reduction)rubocop_cmd.rsuses JSON injection, groups by cop/severity (60%+ reduction)- All three modules use
ruby_exec()fromutils.rsto auto-detectbundle execwhen a Gemfile exists - TOML filter
bundle-install.tomlstripsUsinglines frombundle 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.rsusesrestore_double_dash()fix: Clap strips--but cargo needs it for test flags (e.g.,cargo test -- --nocapture)runner.rsis 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.rsis 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.rsusescore/filterfor language-aware code stripping (FilterLevel: none/minimal/aggressive)search.rsbacks bothrtk grepandrtk rg: it runs the invoked engine (never substituting one for the other) and groups its output, readingcore/configforlimits.grep_max_resultsandlimits.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,--fileswith no other shape flag), whose shared directory prefix is folded into a(N files)header under thenever_worseguard.ctest_cmd.rstakes 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 rawFAILED: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) usescore/filterfor heuristic file summarizationformat_cmd.rsis a cross-ecosystem dispatcher: auto-detects and routes toprettier_cmdorruff_cmd(black is handled inline, not as a separate module)summary.rsexecutes argv directly before summarizing output. Shell-specific syntax requires--shell <name> '<script>'; it never guesses from$SHELL.
Cross-command
format_cmdroutes tocmds/js/prettier_cmdandcmds/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
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 outputZero 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
- Installation — Install RTK and verify you have the right package
- Quick Start — Connect to your AI assistant in 5 minutes
- Supported Agents — Claude Code, Cursor, Copilot, Gemini, and more
Measure your savings
rtk gain # total savings across all sessions
rtk gain --daily # day-by-day breakdown
rtk gain --weekly # weekly aggregationSee Token Savings Analytics for export formats and analysis workflows.
Analyze your usage
rtk discover # find commands that ran without RTK (missed savings)
rtk session # RTK adoption rate per Claude Code sessionSee Discover and Session for details.
Go further
Two optional layers sit on top of the hooks. Neither changes anything until you opt in.
- The engine — everything RTK does is expressed in one composable
pipeline.toml: which commands get rewritten, what happens to a tool's output, what the proxy compresses. With no pipeline file you are onrtk:defaultand nothing changes. Start with Writing a Pipeline; Conditions and Judges and Methods are the vocabulary, Pipeline Commands is for inspecting, debugging and sharing one. - The RTK Proxy — a local daemon that runs the pipeline on the API traffic itself, one layer above bash output. Ships as its own download. See What the Proxy Compresses and Proxy Configuration.
Further reading
- Configuration — config.toml, global flags, env vars, recall recovery
- Troubleshooting — common issues and fixes
- Telemetry & Privacy — what RTK collects and how to opt out
- Agent Coverage — what each agent's hook API lets RTK do
- Editions and Licensing — the three downloads, what each contains, where the licence boundary falls
- ARCHITECTURE.md — system design for contributors
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.
# ~/.config/rtk/pipeline.toml
[pipeline]
extends = ["rtk:posthook", "rtk:proxy"]Tune one thing.
[pipeline]
extends = "rtk:delegate-reads"
[override.shunt-large-read]
when.file_lines_gt = 500This 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.
[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.
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 gainIt 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:
cargo build --release --manifest-path proxy/Cargo.tomlrtk 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
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 downsetup 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:
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
rtk claudeOne 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
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:
rtk llm-proxy start --bind 127.0.0.1:9090Every 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:
rtk llm-proxy config <args...> # get/set runtime settings on a running proxy
rtk llm-proxy service <args...> # manage a per-user background serviceRun 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.
rtk gain # live dashboard (proxy running, interactive terminal)
rtk gain --text # force the static view
rtk gain --text --by-method # which compression method saved whatTELEMETRY (`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
- Once per day (23-hour interval), RTK sends a single HTTPS POST to our telemetry endpoint
- The ping runs in a background thread and never blocks the CLI (2-second timeout)
- A marker file prevents duplicate pings within the interval
- 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)
Consent
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.
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 erasureEnvironment variable override (blocks telemetry regardless of consent):
export RTK_TELEMETRY_DISABLED=1Retention 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 viatracking.history_daysinconfig.toml). Deleted entirely byrtk telemetry forget.
Your Rights (GDPR)
Under the EU General Data Protection Regulation, you have the right to:
- Access your data:
rtk telemetry statusshows 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 forgetto delete local data and send an erasure request to the server. Alternatively, email [email protected] with your device hash. - Restriction of processing:
rtk telemetry disablestops all data collection immediately. - Portability: the local SQLite database at
~/.local/share/rtk/tracking.dbcontains all locally stored data. - Objection:
rtk telemetry disableorexport RTK_TELEMETRY_DISABLED=1.
Erasure Procedure
- 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. - If the server is unreachable, the CLI prints your full device hash and fallback instructions to email [email protected] for manual erasure.
- 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 /erasureendpoint accepting{"device_hash": "...", "action": "erasure"}, authenticated viaX-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_URLis 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:
- Add the query method to
src/core/tracking.rs - Add the field to
EnrichedStatsinsrc/core/telemetry.rs - Populate it in
get_enriched_stats() - Add it to the JSON payload in
send_ping() - Update this document and the README.md privacy table
- 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
- System Overview
- Command Lifecycle
- Module Organization
- Filtering Strategies
- Shared Infrastructure
- Token Tracking System
- Global Flags Architecture
- Error Handling
- Configuration System
- Common Patterns
- Build Optimizations
- Extensibility Guide
- Architecture Decision Records
System Overview
For the proxy pattern diagram and key components table, see TECHNICAL.md.
Design Principles
- Single Responsibility: Each module handles one command type
- Minimal Overhead: ~5-15ms proxy overhead per command
- Exit Code Preservation: CI/CD reliability through proper exit code propagation
- Fail-Safe: If filtering fails, fall back to original output
- Transparent: Users can always see raw output with
-vflags
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:
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 / auditingCommand Lifecycle
Six-Phase Execution Flow
/* Detailed source-code truncated for AI context efficiency. */Verbosity Levels
-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:
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, errTotal: 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
/* Detailed source-code truncated for AI context efficiency. */Code Filtering Levels (src/core/filter.rs)
// 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:
┌────────────────────────────────────────────────────────────────────────┐
│ 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, cargoPython Stack Architecture
Command Implementations
┌────────────────────────────────────────────────────────────────────────┐
│ 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 insteadShared Infrastructure
No Package Manager Detection
Unlike JS/TS modules, Python commands don't auto-detect poetry/pipenv/pip because:
pipis universally available (system Python)uvdetection 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
┌────────────────────────────────────────────────────────────────────────┐
│ 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/vetare semantically related (core Go toolchain)- Mirrors existing git/cargo patterns (consistency)
- Natural CLI:
rtk go testnotrtk 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.
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
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 buildModule 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
┌────────────────────────────────────────────────────────────────────────┐
│ 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, pnpmWhy 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
/* 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
┌────────────────────────────────────────────────────────────────────────┐
│ 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
┌────────────────────────────────────────────────────────────────────────┐
│ 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)
┌────────────────────────────────────────────────────────────────────────┐
│ 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
┌────────────────────────────────────────────────────────────────────────┐
│ 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)
// 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
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)
[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 symbolspanic = "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:
--helpgenerated automatically - Type Safety: Parse arguments directly into typed structs
- Global Flags:
-vand-uwork 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:
- Read the flow once. Start at
CONTRIBUTING.md, then skimdocs/contributing/TECHNICAL.mdto see how a command flows frommain.rs→ a*_cmd.rsfilter → tracking → stdout. - Look at a good example.
src/cmds/git/git_cmd.rsis a representative filter — it shows therun()entry point,LazyLockregex setup, filter helpers, and embedded tests all in one file. - 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.
- Follow the checklist.
src/cmds/README.md — Adding a New Command Filterwalks you through creating a filter, registering it, and adding tests. - 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:
// 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:
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:
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
instacrate. - 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:
#[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.mdwith 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
/* 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:
- Writes a thin shell hook script (e.g.,
~/.claude/hooks/rtk-rewrite.sh) - Stores its SHA-256 hash for integrity verification
- Patches the agent's settings file (e.g.,
settings.json) to register the hook - 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.mdcovers 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):
- The agent fires a
PreToolUseevent (or equivalent) containing the command as JSON - The hook script reads the JSON, extracts the command string
- The hook calls
rtk rewrite "git status"as a subprocess rtk rewriteconsults the command registry and returnsrtk git status- The hook sends a response telling the agent to use the rewritten command
- 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.mdcovers each agent's JSON format, the rewrite registry, compound command handling, and theRTK_DISABLEDoverride.
Rewrite Pipeline
The rewrite pipeline is how RTK intercepts and rewrites commands. The call chain is:
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:
/* 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 -cwrappers 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 haspipeline_final_safemay be rewritten; initially this is limited to ordinarygrepandrginvocations. 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()returnsNoneonly when zero segments were rewritten.
3.3 CLI Parsing and Routing
Once the rewritten command reaches RTK:
- Telemetry:
telemetry::maybe_ping()fires a non-blocking daily usage ping - Clap parsing:
Cli::try_parse()matches against theCommandsenum - Hook check:
hook_check::maybe_warn()warns if the installed hook is outdated (rate-limited to 1/day) - Integrity check:
integrity::runtime_check()verifies the hook's SHA-256 hash for operational commands - Routing: A
match cli.commanddispatches 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:
- Start a timer (
TimedExecution::start()) - Execute the underlying command (
std::process::Command) - Apply filtering (strip boilerplate, group errors, truncate)
- On filter error, fall back to raw output
- Track token savings to SQLite
- Propagate exit code
Details:
src/cmds/README.mdcovers the common pattern, ecosystem organization, cross-command dependencies, and how to add new filters.
3.5 Fallback Path
When Clap parsing fails (unknown command):
- Guard: check if the command is an RTK meta-command (
gain,init, etc.) -- if so, show Clap error - Look up TOML DSL filters via
toml_filter::find_matching_filter() - If TOML match: capture stdout, apply filter pipeline, track savings
- If no match: pure passthrough with
Stdio::inherit, track as 0% output reduction
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.mdcovers 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.mdcovers the analytics modules, andsrc/core/README.mdcovers 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:
- Raw unfiltered output is stored in a content-addressed sqlite database (
~/.local/share/rtk/recall.db, gzip, byte-faithful) - A hint line is printed:
[full output: rtk recall 3f9c2a81d4e7b05a](failures) or[+N hidden: rtk recall <hash>](truncated lists) - 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.mdcovers 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.mdhas the full JSON schemas for each agent.src/hooks/README.mdcovers installation, integrity verification, and the rewrite command.
6. Filter Pipeline Summary
Rust Filters (cmds/**)
Compiled filter modules for complex transformations.
Details:
src/cmds/README.mdand 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.mdcovers 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):
kubectl get pods > tests/fixtures/kubectl_pods_raw.txt2. Write your test in the same module file (#[cfg(test)] mod tests):
#[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):
#[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
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 testsembedded in each module - Fixtures: real command output in
tests/fixtures/ - Integration tests:
#[ignore]attribute, run withcargo test --ignored
For testing requirements, pre-commit gate, and PR checklist, see CONTRIBUTING.md — Testing.
9. Future Improvements
- Extract cli.rs: Move
Commandsenum, 13 sub-enums (GitCommands,CargoCommands, etc.), andAgentTargetfrom 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
- Open a discussion in rtk-ai/rtk Maintainers Applications · Discussions · GitHub titled Maintainer Application: [Your GitHub Handle]
- 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
- For Core Maintainer applications, also include:
- Your experience with Rust
- Your experience with Open Source
- A Core Maintainer will get back to you as soon as possible
- 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
# 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:
--resetis irreversible. It clears both thecommandsandparse_failurestables atomically. A[y/N]confirmation prompt is shown by default. In non-interactive environments (piped stdin), it defaults toNunless--yesis 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
📅 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
📊 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
📆 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
{
"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
# 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.0Use cases:
- Excel analysis
- Python/R data science
- Google Sheets dashboards
- Matplotlib/seaborn plotting
Analysis Workflows
Weekly Progress Tracking
# 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 -3Monthly Cost Analysis
# 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
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
- Export CSV:
rtk gain --all --format csv > rtk-data.csv - Open in Excel
- Create pivot tables:
- Daily trends (line chart)
- Weekly totals (bar chart)
- Savings % distribution (histogram)
Dashboard Creation
# 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>
EOFUnderstanding 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
Input Tokens = estimate_tokens(raw_command_output)
Output Tokens = estimate_tokens(rtk_filtered_output)
Saved Tokens = Input - Output
Savings % = (Saved / Input) × 100Savings % 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
# 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
# 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.sqlCleanup
# 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 databaseIntegration Examples
GitHub Actions CI/CD
# .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 pushSlack Bot
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
# 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 statusExport fails
# 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:
# 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
- Regular Exports:
rtk gain --all --format json > monthly-$(date +%Y%m).json - Trend Analysis: Compare week-over-week savings to identify optimization opportunities
- Command Profiling: Use
--historyto see which commands save the most - Backup Before Cleanup: Always backup before manual database operations
- CI Integration: Track savings across team in shared dashboards
See Also
- README.md - Full rtk documentation
- Token Savings Analytics - The full
rtk gainsurface - How RTK Savings Work - What the numbers measure
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
- Overview
- Global flags
- File commands
- Git commands
- GitHub CLI commands
- Test commands
- Build and lint commands
- Formatting commands
- Package managers
- Containers and orchestration
- Data and network
- Cloud and databases
- Stacked PRs (Graphite)
- Analytics and tracking
- Hook system
- Configuration
- Recall system (output recovery)
- 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:
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 validationFile commands
`rtk ls` -- Directory listing
Purpose: Replaces ls and tree with token-optimized output.
Syntax:
rtk ls [args...]All native ls flags are supported (-l, -a, -h, -R, etc.).
Before / After:
# 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:
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:
rtk read <file> [options]
rtk read - [options] # Read from stdinOptions:
| 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):
# 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:
rtk smart <file> [--model heuristic] [--force-download]Example:
$ rtk smart src/tracking.rs
SQLite-based token tracking system for command executions.
Records input/output tokens, savings %, execution times with 90-day retention.`rtk find` -- File search
Purpose: Replaces find and fd with compact output grouped by directory.
Syntax:
rtk find [args...]Supports both RTK syntax and native find syntax (-name, -type, etc.).
Before / After:
# 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...`rtk grep` -- Content search
Purpose: Replaces grep and rg with truncated output grouped by file.
Syntax:
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:
# 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(...)`rtk ast-grep` -- Structural (AST) search
Purpose: Replaces ast-grep with capped output grouped by file.
Syntax:
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:
rtk diff <file1> <file2>
rtk diff - # Condense a unified diff read on stdinPour 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:
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
rtk git status [args...] # Supports all git status flagsBefore / After:
# 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
rtk git log [args...] # Supports --oneline, --graph, --all, -n, etc.Before / After:
# 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
rtk git diff [args...] # Supports --stat, --cached, --staged, etc.Before / After:
Raw git diff:
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:
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 -2On 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:+oursis counted in the+N -Mtotal but stays invisible togrep "^+". - The output is not an applicable patch: rtk removes the
diff --git/---/+++headers. Usertk bypass git difffor a faithful patch, orrtk gh pr diff --patch, which passes through unfiltered.
`rtk git show` -- Compact show
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-writtenrtk git show HEAD:x > filecan therefore silently
truncate the content (the exit code stays 0). To capture the full
file, follow the recovery hint, or usertk proxy git show. A baregit showis not enough when the RTK hook is active: it is rewritten tortk git show, which windows the output again.
`rtk git add` -- Compact add
rtk git add [args...] # Supports -A, -p, --all, etc.Output: ok (a single word)
`rtk git commit` -- Compact commit
rtk git commit -m "message" [args...] # Supports -a, --amend, --allow-empty, etc.Output: ok abc1234 (confirmation + short hash)
`rtk git push` -- Compact push
rtk git push [args...] # Supports -u, remote, branch, etc.Before / After:
# 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
rtk git pull [args...]Output: ok 3 files +10 -2
`rtk git branch` -- Compact branches
rtk git branch [args...] # Supports -d, -D, -m, etc.Shows the current branch, local branches and remote branches compactly.
`rtk git fetch` -- Compact fetch
rtk git fetch [args...]Output: ok fetched (N new refs)
`rtk git stash` -- Compact stash
rtk git stash [list|show|pop|apply|drop|push] [args...]`rtk git worktree` -- Compact worktree
rtk git worktree [add|remove|prune|list] [args...]Git passthrough
Any git subcommand not listed above is run directly:
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.0GitHub CLI commands
`rtk gh` -- Compact GitHub CLI
Purpose: Replaces gh with optimized output.
Syntax:
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:
# 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:
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:
rtk test cargo test
rtk test npm test
rtk test bun test
rtk test pytestBefore / After:
# 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:
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:
rtk err npm run build
rtk err cargo build`rtk cargo test` -- Rust tests
rtk cargo test [args...]Shows only failures. Supports all cargo test arguments.
`rtk cargo nextest` -- Rust tests (nextest)
rtk cargo nextest [run|list|--lib] [args...]Filters cargo nextest output to show only failures.
`rtk jest` / `rtk vitest` -- Jest/Vitest tests
rtk jest [args...]
rtk vitest [args...]`rtk playwright test` -- Playwright E2E tests
rtk playwright [args...]`rtk pytest` -- Python tests
rtk pytest [args...]`rtk go test` -- Go tests
rtk go test [args...]Uses Go's NDJSON streaming output for precise filtering.
Build and lint commands
`rtk cargo build` -- Rust build
rtk cargo build [args...]Removes "Compiling..." lines, keeping only errors and the final result.
`rtk cargo check` -- Rust check
rtk cargo check [args...]Removes "Checking..." lines, keeping only errors.
`rtk cargo clippy` -- Rust Clippy
rtk cargo clippy [args...]Groups warnings by lint rule.
`rtk cargo install` -- Rust install
rtk cargo install [args...]Removes dependency compilation, keeping only the install result and errors.
`rtk tsc` -- TypeScript compiler
rtk tsc [args...]Groups TypeScript errors by file and error code.
Before / After:
# 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
rtk lint [args...]
rtk lint biome [args...]Groups violations by rule and by file. Auto-detects the linter.
`rtk prettier` -- Format checking
rtk prettier [args...] # e.g. rtk prettier --check .Shows only the files that need formatting.
`rtk format` -- Universal formatter
rtk format [args...]Auto-detects the project formatter (prettier, black, ruff format) and applies a compact filter.
`rtk next build` -- Next.js build
rtk next [args...]Compact output with route metrics.
`rtk ruff` -- Python linter/formatter
rtk ruff check [args...]
rtk ruff format --check [args...]Compressed JSON output.
`rtk mypy` -- Python type checker
rtk mypy [args...]Groups type errors by file.
`rtk golangci-lint` -- Go linter
rtk golangci-lint run [args...]Compressed JSON output.
Formatting commands
`rtk prettier` -- Prettier
rtk prettier --check .
rtk prettier --write src/`rtk format` -- Universal detector
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
rtk npm [args...] # e.g. rtk npm run buildFilters npm boilerplate (progress bars, headers, etc.).
`rtk npx` -- npx with smart routing
rtk npx [args...]Routes to specialized filters:
rtk npx tsc-> tsc filterrtk npx eslint-> lint filterrtk npx prisma-> prisma filter- Others -> filtered passthrough
`rtk pip` -- pip / uv
rtk pip list # Package list (auto-detects uv)
rtk pip outdated # Outdated packages
rtk pip install <pkg> # InstallAuto-detects uv when available and uses it instead of pip.
`rtk deps` -- Dependency summary
Purpose: Compact summary of project dependencies.
rtk deps [path] # Default: current directoryAuto-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:
# 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.
rtk json <file> [--depth N] # Default: depth 5
rtk json - # From stdinBefore / After:
# 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
rtk env # All variables (sensitive ones masked)
rtk env -f AWS # Filter by name
rtk env --show-all # Include sensitive valuesSensitive variables (tokens, secrets, passwords) are masked by default: AWS_SECRET_ACCESS_KEY=***.
`rtk log` -- Deduplicated logs
Purpose: Filters and deduplicates log output.
rtk log <file> # From a file
rtk log # From stdin (pipe)Repeated lines are merged: [ERROR] Connection refused (x42).
`rtk curl` -- HTTP with truncation
rtk curl [args...]Truncates long responses and saves the full output for recovery.
`rtk wget` -- Compact download
rtk wget <url> [args...]
rtk wget -O - <url> # Output to stdoutRemoves progress bars and noise.
`rtk summary` -- Heuristic summary
Purpose: Runs a command and generates a heuristic summary of its output.
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.
rtk bypass <command...>Useful for debugging: compare raw output with filtered output.
Cloud and databases
`rtk aws` -- AWS CLI
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
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
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 exportOptions:
| 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):
$ 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.
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 exportOptions:
| 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.
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.
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.
rtk hook-audit # Last 7 days (default)
rtk hook-audit --since 30 # Last 30 days
rtk hook-audit --since 0 # Full historyHook 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:
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 ClaudeKey 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
rtkpass through unchanged - Heredocs (
<<) are not modified - Unrecognized commands pass through unchanged
Installation
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 # UninstallInstalled 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 asking1: 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 unchanged2: 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 message3: 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.
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.
rtk verifyAutomatically 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:
[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" coversplaywright test as well as npx playwright test or pnpm exec playwright test, and "pytest" also coverspython3 -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:
rtk config # Show the current configuration
rtk config --create # Create the file with default valuesFull structure
[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 rewritingEnvironment 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:
- The command fails (exit code != 0) or the output is truncated
- RTK stores the raw output (gzip) in
~/.local/share/rtk/recall.db - A hint with the hash is shown:
[full output: rtk recall <hash>]or[+N hidden: rtk recall <hash>] rtk recall <hash>returns the missing part (delta);--fullreturns the complete output
Commands:
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:
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 erasureDisable via environment variable:
export RTK_TELEMETRY_DISABLED=1No 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:
[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 towardcommands 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
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 commandsSee Token Savings Analytics for the full rtk gain surface.
Exporting
rtk gain --all --format json > savings.json
rtk gain --all --format csv > savings.csvExample JSON entry for one day (illustrative values):
{
"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:
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).dbReset
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:
rm ~/.local/share/rtk/history.dbTroubleshooting
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.
rtk discover # analyze current project history
rtk discover --all # all projects
rtk discover --all --since 7 # last 7 days, all projectsExample output (illustrative values):
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.
rtk sessionExample output (illustrative values):
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
# 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
rtk gain --dailyExample output (illustrative values — yours depend entirely on which commands you run):
📅 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
rtk gain --weekly
rtk gain --monthlySame 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:
{
"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.
Input Tokens = estimate_tokens(raw_command_output)
Output Tokens = estimate_tokens(rtk_filtered_output)
Saved Tokens = Input - Output
Savings % = (Saved / Input) × 100Database
Savings data is stored locally in SQLite:
- Location:
~/.local/share/rtk/history.dbon Linux,~/Library/Application Support/rtk/history.dbon macOS (override withRTK_DB_PATH) — paths below use the Linux location - Retention: 90 days (automatic cleanup)
- Scope: Global across all projects and Claude sessions
# 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 commandPrefer --reset over deleting the file: it zeroes both the token savings and the
recall stats, which live separately, and leaves the database intact.
rtk gain --reset # asks for confirmation
rtk gain --reset --yes # skip the prompt (scripts)Analysis workflows
# 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.jsonPython/pandas:
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):
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 pushQuota 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.
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 budgetThe 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.
rtk gain --watchAuto-launch
A bare rtk gain launches the dashboard automatically once the RTK Proxy is running. All four conditions must hold:
- You passed no flag asking for a specific static view — no
--daily,--weekly,--monthly,--all,--history,--graph,--quota, and--formatleft attext. - You did not pass
--text. - Standard output is an interactive terminal — piping or redirecting opts out, so scripts and cron jobs are unaffected.
- 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:
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.
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 pipelineThese 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.
rtk gain --pipeline my-pipelineShadow 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:
rtk gain --shadowA 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:
rtk gain --recallsIllustrative output:
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:
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 dataIncorrect statistics: Token estimation is a heuristic. For precise counts, use tiktoken:
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 |
rtk config # show current configuration
rtk config --create # create config file with defaultsFull config structure
/* 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. |
[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:
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:
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 storageSetting 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:
[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" coversvendor/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 ofgit), 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:
[hooks]
exclude_commands = ["^curl", "^wget", "git rebase"]Invalid regex patterns fall back to prefix matching.
Or for a single invocation:
RTK_DISABLED=1 git rebase mainTelemetry
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:
rtk telemetry disable # withdraw consent
export RTK_TELEMETRY_DISABLED=1 # block telemetry regardless of configCustom filters
Add your own filters (or override built-ins) in either location:
- Project-local —
.rtk/filters.tomlin 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:
rtk trust # shows each filter and asks to confirm (--yes to skip the prompt)
rtk untrust # revokes trustrtk 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.tomlwithout trust. After upgrading, the user-global file is gated like project filters — if you already relied on a global filter, runrtk trustonce 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
rtk --version # should print: rtk x.y.z
rtk gain # should show the savings dashboardIf both commands work, RTK is already installed. Skip to Project initialization.
Quick install (Linux and macOS)
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | shInstalls to ~/.local/bin. Add it to your PATH if it is not there already:
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrcHomebrew (macOS and Linux)
brew install rtk-ai/tap/rtkwinget (Windows)
winget install rtk-ai.rtkCargo
:::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:
:::
cargo install --git https://github.com/rtk-ai/rtk --branch master rtkPre-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.
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-featuresRelease 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.
# 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 -gUpgrading 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:
winget install BurntSushi.ripgrep.MSVCWSL
WSL also works and behaves exactly like Linux:
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
rtk --version # rtk x.y.z
rtk gain # savings dashboardIf rtk gain fails but rtk --version succeeds, you installed Rust Type Kit by mistake. Uninstall it first:
cargo uninstall rtkThen 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:
rtk initTo install the Claude Code hook (registered in ~/.claude/settings.json, applies to every project):
rtk init --globalUninstall
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:
rtk --version # rtk x.y.z
rtk gain # shows token savings dashboardIf not, see Installation.
Step 1: Initialize for your AI assistant
# 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 initrtk 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:
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 CodeSee 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:
rtk init --global --dry-runEvery 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:
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:
rtk gainIllustrative output:
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 -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:
fish -c 'git status; cargo test' # → fish -c 'rtk git status; rtk cargo test'
fish -c 'git status; and cargo test' # unchangedShell 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:
rtk bypass make installNext steps
- What RTK Optimizes — all supported commands and savings by ecosystem
- Supported agents — Claude Code, Cursor, Copilot, and more
- Configuration — customize RTK behavior
- Token Savings Analytics — the full
rtk gainsurface - Editions and Licensing — what each download contains
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.
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 outputSupported 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 inconfig.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
rtk init --global # installs hook + patches settings.jsonRestart Claude Code. Verify:
rtk init --show # shows hook statusTrae
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:
rtk init --uninstall --agent trae
rtk init --uninstall --global --agent traeUninstall removes only RTK-managed hook entries; unrelated Trae hooks and configuration are preserved.
Cursor
rtk init --global --agent cursorRestart Cursor. The hook uses preToolUse with Cursor's updated_input format.
GitHub Copilot (VS Code Chat + CLI)
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:
rtk init --uninstall --copilot
rtk init --uninstall --global --copilotRemoves 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
rtk init --global --geminiOpenCode
rtk init --global --opencodeInstalls 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
# Project-local (default)
rtk init --agent pi
# Global — all projects
rtk init --agent pi --globalCreates .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:
rtk init --uninstall --agent pi
rtk init --uninstall --agent pi --globalRemoves 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)
# Project-local (default)
rtk init --agent omp
# Global — all projects
rtk init --agent omp --globalCreates .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:
rtk init --uninstall --agent omp
rtk init --uninstall --agent omp --globalRemoves 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
openclaw plugins install ./openclawPlugin 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
rtk init --agent hermesCreates ~/.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
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:
rtk init --uninstall -g --agent droid
rtk init --uninstall --agent droidRemoves only RTK's hook entry; other hooks and settings are untouched.
Cline / Roo Code
rtk init --agent cline # creates .clinerules in current projectCline reads .clinerules as custom instructions. RTK adds guidance telling Cline to prefer rtk <cmd> over raw commands.
Windsurf
rtk init --global --agent windsurf # creates .windsurfrules in current projectCodex CLI
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 integrationRestart 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
rtk init --agent kilocode # creates .kilocode/rules/rtk-rules.md in current projectKilo 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)
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:
rtk init --agent antigravity --uninstall # workspace
rtk init -g --agent antigravity --uninstall # globalMistral Vibe
rtk init -g --agent vibe # user-scoped (~/.vibe/hooks.toml)
rtk init -g --agent vibe --hook-only # skip the ~/.vibe/prompts/rtk.md prompt fileInstalls 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:
rtk init -g --agent vibe --uninstallStrips 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
RTK_DISABLED=1 git status # runs raw git status, no rewriteOr exclude commands permanently in ~/.config/rtk/config.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
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 logInspecting what is active
rtk pipeline showPrints the pipeline that actually resolved for this directory. To print a built-in verbatim instead, name it:
rtk pipeline show rtk:default
rtk pipeline show rtk:posthookValidating your own pipelines
validate exits non-zero on errors, which makes it usable as a pre-commit or CI check:
rtk pipeline validatetest 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:
rtk pipeline test
rtk pipeline statusValidation 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:
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:
rtk pipeline explain --tool Grep < output.txtIt 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
[[hook.after_tool.shell]]
name = "gentle-term"
when = { is_rtk_command = false }
use = ["ansi", "dedup"]
shadow = trueThe 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:
rtk pipeline bench my-output.txtUse 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:
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 offEach 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).
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_commandEnable 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.
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 terminalThe 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.tomlplus a README card. The repo description carries the one-line install command, the repo gets thertk-pipelinetopic sortk pipeline searchfinds 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/repoline (theirghlogin 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.
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.
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:
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 appliedinstalled: ~/.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
rtk pipeline search # shared pipelines, most-starred first
rtk pipeline search rust # filtered by a free-text queryDiscovery 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.
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 # disarmPipeline 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 candidateBoth 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.
Related commands
rtk methods list # every registered method with its contract
rtk tasks list # every configured task with its kind and upstreamGuide 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:
[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 |
[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 toinput_charsof 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), everyinput.*field — for Bash, the agent's owndescriptionof 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 samewhen(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_envnames the credential;rtk pipeline validate,rtk trustandrtk initprintauth needed: export VAR=...for anything unset. See Tasks and Upstreams forapi_key_commandand 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:
[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.
[[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:
~/.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
rtkprefix; 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 andvalidatesays so (W005).- A
routerule 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
[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.
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 rewritesmytool deploy → rtk mytool deploy (it consults every filter'smatch_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
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 carriesThen name the method in a rule, exactly like a built-in:
[[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 withcargo 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:
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);applyreceives the block'stext(plus its subject, format, and provenance)
and returns anOutcome:Continue(String)to pass the transformed text down
the chain,ShortCircuit(String)to stop the chain, orSkipto 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.RecallorNone),cost, and what the method consumes/produces. UseMethodInfo::LOSSLESS_TRANSFORMas 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:
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:
- In each plugin's
Cargo.toml, set thertk-plugindependency'stagto
the new version (rtk --version→vX.Y.Z).rtk plugins newwrites the
tag once, at scaffold time. - Run
rtk plugins buildagain.
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 scaffoldedCargo.tomldepends onrtk-pluginby git, pinned to the tag
matching your installedrtk, so your method always links the exact engine
yourrtkruns. - A plugin adds methods and task kinds, not conditions. Every field of a
tool's input is already a condition via theinput.*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 withrtk trustbefore 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.
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 = 512How 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.
[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).
[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
[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:
[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 runrtk check-tests --question "…" -- npm test; the exit code still comes fromnpm 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; anafter_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
[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
[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 |
[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:
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
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 limitsA 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
[[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 |
[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.
swapneeds 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.answerwaits for the worker inside the hook, bounded by the upstream'stimeout_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 withoffsetandlimit, since a ranged read is notwhole_file.- Any other combination is refused (E005):
swapis for commands only, andansweron a shell command is whatswapdoes 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.
[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.
[[proxy.request.tool_result]]
name = "slim-web-pages"
when = { tool = "web" }
use = ["web-md", "md-slim"]Two limits apply:
Readresults are never rewritten — a laterEditneeds 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 underhook.after_toolinstead.rtk pipeline validatereports 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,
[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.
[[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) |
rtk llm-proxy config getThe 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
- Copilot CLI: output replacement relies on Copilot CLI's documented
modifiedResultfield. - 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.
- Mistral Vibe: replacement rides on a
denydecision, reaches only the text form of the result, and the UI shows it as a warning. - Gemini CLI: input edits merge rather than replace, and each one makes Gemini append a
[System] Tool input parameters were modifiednote to the output — so rewriting costs a few tokens back. - Codex: the rewrite is honoured only alongside an explicit
allowdecision. Without it the hook fails and the original command runs unwrapped. - Windsurf: now Devin Desktop. Older Windsurf installs are block-only and cannot rewrite.
- 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.
- Rules file only: for Kilo Code, Cline, Windsurf / Devin, Kimi Code and Antigravity,
rtk initwrites a rules file asking the model to preferrtk <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:
[[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,
PostToolUsereturns{}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
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:
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | RTK_EDITION=oss shHomebrew ships it as a separate formula:
brew install rtk-ai/tap/rtk-ossDebian 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:
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
rtkCLI and its filters. - BUSL-1.1 covers the compression engine and the
rtk-proxydaemon.
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.
agent runs a shell command
|
v
RTK filters the output
|
v
agent reads the resultThe 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
Cost
├─ Input tokens
│ ├─ Bash output <- the only part RTK filters
│ ├─ Your prompt
│ ├─ System prompt
│ └─ Conversation history
└─ Output tokens <- what the model writesThose 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,000as 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
- What RTK Optimizes — what each supported command filter changes
- Token Savings Analytics — reading the
rtk gaindashboard
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
- Once per day (23-hour interval), RTK sends a single HTTPS POST to the telemetry endpoint
- The ping runs in a background thread and never blocks the CLI (2-second timeout)
- A marker file prevents duplicate pings within the interval
- 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)
Consent
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.
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 erasureEnvironment variable override (blocks telemetry regardless of consent):
export RTK_TELEMETRY_DISABLED=1Retention 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 viatracking.history_daysinconfig.toml). Deleted entirely byrtk telemetry forget.
Your Rights (GDPR)
Under the EU General Data Protection Regulation, you have the right to:
- Access your data:
rtk telemetry statusshows 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 forgetto delete local data and send an erasure request to the server. Alternatively, email [email protected] with your device hash. - Restriction of processing:
rtk telemetry disablestops all data collection immediately. - Portability: the local SQLite database at
~/.local/share/rtk/history.dbcontains all locally stored data. - Objection:
rtk telemetry disableorexport RTK_TELEMETRY_DISABLED=1.
Erasure Procedure
- 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. - If the server is unreachable, the CLI prints your full device hash and fallback instructions to email [email protected] for manual erasure.
- 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:
$ 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:
cargo uninstall rtk
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
rtk gain # should now show token savings statsHow 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:
Verify RTK is installed:
bashrtk --version rtk gainInitialize the hook:
bashrtk init --global # Claude Code rtk init --global --agent cursor # Cursor rtk init --global --opencode # OpenCodeRestart your AI assistant.
Verify hook status:
bashrtk init --showCheck
settings.jsonhas the hook registered (Claude Code):bashcat ~/.claude/settings.json | grep rtk
RTK not found after `cargo install`
Symptom:
$ rtk --version
zsh: command not found: rtkCause: ~/.cargo/bin is not in your PATH.
Fix:
For bash (~/.bashrc) or zsh (~/.zshrc):
export PATH="$HOME/.cargo/bin:$PATH"For fish (~/.config/fish/config.fish):
set -gx PATH $HOME/.cargo/bin $PATHThen reload:
source ~/.zshrc # or ~/.bashrc
rtk --versionRTK 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, typecmd, 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:
rtk init -g # answer y at the settings.json prompt
rtk init -g --auto-patch # or: patch settings.json without askingThe 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:
rtk init --showIt 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:
rtk vitest --run
Error: program not foundCause: 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+:
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
rtk --version # should be 0.23.1+Compilation error during installation
rustup update stable
rustup default stable
cargo clean
cargo build --release
cargo install --path . --forceMinimum required Rust version: 1.91 (edition 2024 needs Cargo 1.85 or newer).
OpenCode not using RTK
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 readopencode --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. SetRTK_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:
cargo install --git https://github.com/rtk-ai/rtk --branch master rtkDoes 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:
rtk cc-economicsrtk gain reports token savings only; the cache breakdown lives in rtk cc-economics.
Run the diagnostic script
From the RTK repository root:
bash scripts/check-installation.shChecks:
- 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 |
Files and Search
| 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) |
# Debug: see what RTK is doing
rtk -vvv git statusCommands 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:
rtk bypass make install # runs make install, tracks usage, no filteringTo check which commands were missed opportunities: rtk discover.
--- METRICS ---
- Files Extracted: 71
- Estimated Token Budget: ~119157 tokens
- Canonical Reference: https://codewiki.google/github.com/rtk-ai/rtk