# Technical Documentation: rtk-ai/rtk

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/rtk-ai/rtk)

### Overview (`README.md`)

<p align="center">
  <img src="https://avatars.githubusercontent.com/u/258253854?v=4" alt="RTK - Rust Token Killer" width="500">
</p>

<p align="center">
  <strong>Agent-agnostic token optimization: compress, route, filter, and more.</strong>
  <br><br>
  One composable pipeline across everything your AI agent reads and sends — built on RTK's battle-tested bash-output compression (up to 90% smaller).
</p>

<p align="center">
  <a href="https://github.com/rtk-ai/rtk/actions"><img src="https://github.com/rtk-ai/rtk/workflows/Security%20Check/badge.svg" alt="CI"></a>
  <a href="https://github.com/rtk-ai/rtk/releases"><img src="https://img.shields.io/github/v/release/rtk-ai/rtk" alt="Release"></a>
  <a href="#license"><img src="https://img.shields.io/badge/license-Apache--2.0%20%2B%20BUSL--1.1-blue.svg" alt="License: Apache-2.0 and BUSL-1.1"></a>
  <a href="https://discord.gg/RySmvNF5kF"><img src="https://img.shields.io/discord/1470188214710046894?label=Discord&logo=discord" alt="Discord"></a>
  <a href="https://formulae.brew.sh/formula/rtk"><img src="https://img.shields.io/homebrew/v/rtk" alt="Homebrew"></a>
</p>

<p align="center">
  <a href="https://www.rtk-ai.app">Website</a> &bull;
  <a href="#installation">Install</a> &bull;
  <a href="https://www.rtk-ai.app/guide/resources/troubleshooting">Troubleshooting</a> &bull;
  <a href="docs/contributing/ARCHITECTURE.md">Architecture</a> &bull;
  <a href="https://discord.gg/RySmvNF5kF">Discord</a>
</p>

<p align="center">
  <a href="README.md">English</a> &bull;
  <a href="README_fr.md">Francais</a> &bull;
  <a href="README_zh.md">中文</a> &bull;
  <a href="README_ja.md">日本語</a> &bull;
  <a href="README_ko.md">한국어</a> &bull;
  <a href="README_es.md">Espanol</a> &bull;
  <a href="README_pt.md">Português</a>
</p>

---

<p align="center">
 <a href="https://www.star-history.com/rtk-ai/rtk">
  <picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=rank&theme=dark" /><source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=rank" /><img alt="Star History Rank" src="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=rank" /></picture> <picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=trending&theme=dark" /><source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=trending" /><img alt="GitHub Trending Repository of the Day" src="https://api.star-history.com/badge?repo=rtk-ai/rtk&type=trending" /></picture>
 </a>
</p>

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](docs/guide/resources/savings-explained.md)
## Installation

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

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

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

> Cargo, pre-built binaries, native Windows, WSL, and uninstall: **[Installation guide](https://www.rtk-ai.app/guide/getting-started/installation)**

## What You Install

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

```mermaid
flowchart TD
    agent["Your coding agent"]

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

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

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

    classDef apache stroke:#2da44e,stroke-width:2px,fill:none
    classDef busl stroke:#0969da,stroke-width:2px,fill:none
    classDef mixed stroke:#8250df,stroke-width:2px,fill:none
    classDef plain stroke:#8c959f,stroke-width:1px,fill:none
    class full mixed
    class oss apache
    class proxy busl
    class agent plain
    style editions fill:none,stroke:#d0d7de,stroke-dasharray:4 4
```

| Download | Licence | Contains |
|---|---|---|
| `rtk-<target>` | Apache-2.0 + BUSL-1.1 | Hook core, every command filter, **and** the compression engine |
| `rtk-oss-<target>` | Apache-2.0 only | Hook core and every command filter. No engine. |
| `rtk-proxy-<target>` | BUSL-1.1 | The RTK Proxy daemon. Never bundled with either `rtk`. |

**Production use is free**, engine and daemon included. What the BUSL grant does not cover is offering RTK or a derivative to third parties as a commercial product or service — for that, contact **contact@rtk-ai.app**.

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

> What the Apache-2.0 edition omits and where the licence boundary falls: **[Editions and Licensing](https://www.rtk-ai.app/guide/resources/editions)**

## Quick Start

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

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

The agent receives compact output without ever calling `rtk` explicitly.

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

> Per-agent setup, `--dry-run`, and uninstall: **[Quick Start](https://www.rtk-ai.app/guide/getting-started/quick-start)** · **[Supported Agents](https://www.rtk-ai.app/guide/getting-started/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:

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

> **Does RTK break Claude's prompt cache?** No. RTK filters output once per command. The result is stored in history and cached normally on subsequent API calls, so the cache keeps working as expected. Smaller outputs also mean cheaper cache writes and reads. See [Troubleshooting](docs/guide/resources/troubleshooting.md#does-rtk-break-claudes-prompt-cache) 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](https://www.rtk-ai.app/guide/getting-started/supported-agents). The Hermes plugin source and tests live in `hooks/hermes/`; installed Hermes runtime files still live under `~/.hermes/plugins/rtk-rewrite/`.
## What RTK Covers

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

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

> Every command and its filter: **[What RTK Optimizes](https://www.rtk-ai.app/guide/resources/what-rtk-covers)**

## Measure It

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

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

rtk session                     # Show RTK adoption across recent sessions
```

> Attribution, the live dashboard and export formats: **[Token Savings Analytics](https://www.rtk-ai.app/guide/analytics/gain)**

## RTK Proxy (optional)

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

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

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

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

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

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

> Setup and subcommands: **[RTK Proxy](https://www.rtk-ai.app/guide/proxy/overview)** · what it compresses: **[What the Proxy Compresses](https://www.rtk-ai.app/guide/proxy/compression)** · settings and endpoints: **[Proxy Configuration](https://www.rtk-ai.app/guide/proxy/configuration)**

## The Engine (`pipeline.toml`)

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

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

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

> The tour: **[Pipelines](https://www.rtk-ai.app/guide/pipelines/overview)** · the reference: **[Writing a Pipeline](https://www.rtk-ai.app/guide/pipelines/writing-pipelines)**, **[Conditions and Judges](https://www.rtk-ai.app/guide/pipelines/conditions)**, **[Methods](https://www.rtk-ai.app/guide/pipelines/methods)**, **[Tasks and Upstreams](https://www.rtk-ai.app/guide/pipelines/tasks)** · the commands: **[Pipeline Commands](https://www.rtk-ai.app/guide/pipelines/commands)** · write your own method: **[Plugins](https://www.rtk-ai.app/guide/pipelines/plugins)**

## Documentation

**[rtk-ai.app/guide](https://www.rtk-ai.app/guide)** is the full user guide.

| | |
|---|---|
| [Installation](https://www.rtk-ai.app/guide/getting-started/installation) | every install method, Windows, WSL, uninstall |
| [Quick Start](https://www.rtk-ai.app/guide/getting-started/quick-start) | per-agent init, `--dry-run`, first savings |
| [Supported Agents](https://www.rtk-ai.app/guide/getting-started/supported-agents) | setup detail, override controls, degradation |
| [Agent Coverage](https://www.rtk-ai.app/guide/resources/agent-coverage) | what each agent's hook API lets RTK do |
| [What RTK Optimizes](https://www.rtk-ai.app/guide/resources/what-rtk-covers) | every command and its filter |
| [Configuration](https://www.rtk-ai.app/guide/getting-started/configuration) | `config.toml`, env vars, recall recovery |
| [Token Savings Analytics](https://www.rtk-ai.app/guide/analytics/gain) | `rtk gain` in full |
| [Pipelines](https://www.rtk-ai.app/guide/pipelines/overview) | the engine: `pipeline.toml`, rules, judges, tasks, sharing |
| [RTK Proxy](https://www.rtk-ai.app/guide/proxy/overview) | the daemon: setup, what it compresses, settings |
| [Editions and Licensing](https://www.rtk-ai.app/guide/resources/editions) | what each download contains |
| [Telemetry & Privacy](https://www.rtk-ai.app/guide/resources/telemetry) | what is collected, how to opt in, your GDPR rights |
| [Troubleshooting](https://www.rtk-ai.app/guide/resources/troubleshooting) | common issues, prompt-cache questions |

Repository references: [engine/README.md](engine/README.md) · [proxy/README.md](proxy/README.md) · [ARCHITECTURE.md](docs/contributing/ARCHITECTURE.md) · [CONTRIBUTING.md](CONTRIBUTING.md) · [SECURITY.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](https://www.rtk-ai.app/guide/resources/telemetry).

## Star History

<a href="https://www.star-history.com/?repos=rtk-ai%2Frtk&type=date&legend=top-left">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=rtk-ai/rtk&type=date&theme=dark&legend=top-left" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=rtk-ai/rtk&type=date&legend=top-left" />
   <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=rtk-ai/rtk&type=date&legend=top-left" />
 </picture>
</a>

## StarMapper

<a href="https://starmapper.bruniaux.com/rtk-ai/rtk">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://starmapper.bruniaux.com/api/map-image/rtk-ai/rtk?theme=dark" />
    <source media="(prefers-color-scheme: light)" srcset="https://starmapper.bruniaux.com/api/map-image/rtk-ai/rtk?theme=light" />
    <img alt="StarMapper" src="https://starmapper.bruniaux.com/api/map-image/rtk-ai/rtk" />
  </picture>
</a>

## Core team

- **Patrick Szymkowiak** — Founder
  [GitHub](https://github.com/pszymkowiak) · [LinkedIn](https://www.linkedin.com/in/patrick-szymkowiak/)
- **Florian Bruniaux** — Core contributor
  [GitHub](https://github.com/FlorianBruniaux) · [LinkedIn](https://www.linkedin.com/in/florian-bruniaux-43408b83/)
- **Adrien Eppling** — Core contributor
  [GitHub](https://github.com/aeppling) · [LinkedIn](https://www.linkedin.com/in/adrien-eppling/)
- **Nicolas Le Cam** — Core contributor
  [Github](https://github.com/kush) · [LinkedIn](https://www.linkedin.com/in/nicolas-le-cam-386387160/)
- **Takayuki Maeda** — Core contributor
  [GitHub](https://github.com/TaKO8Ki) · [LinkedIn](https://www.linkedin.com/in/tako8ki/)

## Contributing

Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/rtk-ai/rtk).

Join the community on [Discord](https://discord.gg/RySmvNF5kF).

## License

The `rtk` CLI is **Apache-2.0** — see [LICENSE](LICENSE).

It links one component under a different licence. `rtk-engine` (`engine/`) is
**BUSL-1.1**, source-available: see [engine/LICENSE](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](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/`](../docs/guide/pipelines/overview.md), published at [rtk-ai.app/guide](https://www.rtk-ai.app/guide/pipelines/overview):

| Page | Covers |
|---|---|
| [Pipelines](../docs/guide/pipelines/overview.md) | what the engine does, the files you might write, upgrading |
| [Writing a Pipeline](../docs/guide/pipelines/writing-pipelines.md) | file locations and load order, the rule tree, actions, routing, overrides |
| [Conditions and Judges](../docs/guide/pipelines/conditions.md) | every `when` key, the call context, classifiers |
| [Methods](../docs/guide/pipelines/methods.md) | built-in methods and chains, your own chains and filters |
| [Directives](../docs/guide/pipelines/directives.md) | text RTK puts in front of the agent |
| [Tasks and Upstreams](../docs/guide/pipelines/tasks.md) | delegation to a cheaper model |
| [Reference Pipelines](../docs/guide/pipelines/reference-pipelines.md) | the six built-ins as literal files |
| [Pipeline Commands](../docs/guide/pipelines/commands.md) | show, validate, explain, shadow, share, adopt |
| [Errors and Warnings](../docs/guide/pipelines/errors.md) | every diagnostic code, limits |
| [Plugins](../docs/guide/pipelines/plugins.md) | new methods and task kinds in Rust |

Worked pipelines: [`examples/pipelines/`](../examples/pipelines/README.md).

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md). `rtk methods list` prints every registered method with its contract.

## License

[Business Source License 1.1](LICENSE) — 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 contact@rtk-ai.app. 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](../docs/guide/resources/editions.md).

---

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

# LLM Agent Hooks

## Scope

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

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

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

## Awareness files

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

## Purpose

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

## How It Works

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

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

## Directory Structure

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

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

## Supported Agents

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

## JSON Formats by Agent

### Claude Code (Shell Hook)

**Input** (stdin):

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

**Output** (stdout, when rewritten):

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

### Cursor (Shell Hook)

**Input**: Same as Claude Code.

**Output** (stdout, when rewritten):

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

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

### Trae (Rust Binary)

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

**Input**: Trae `PreToolUse` payloads for `RunCommand`, such as:

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

**Output** (only when rewritten):

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

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

### Codex CLI (Rust Binary)

**Input** (stdin):

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

**Output** (stdout, when rewritten):

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

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

### Copilot CLI (Rust Binary)

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

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

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

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

### VS Code Copilot Chat (Rust Binary)

**Input** (stdin, snake_case):

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

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

### Gemini CLI (Rust Binary)

**Input** (stdin):

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

**Output** (when rewritten):

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

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

### Mistral Vibe (Rust Binary)

**Input** (stdin):

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

**Output** (when rewritten):

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

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

### Google Antigravity (Rust Binary)

**Input** (stdin):

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

**Output** (when rewritten):

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

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

### OpenCode (TypeScript Plugin)

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

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

### Hermes (Python Plugin)

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

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

## Command Rewrite Registry

The registry handles command patterns across these categories:

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


### Compound Command Handling

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

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

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

### Override Controls

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

## Exit Code Contract

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

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

## Graceful Degradation

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

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

## Adding a New Agent Integration

New integrations must follow the [Exit Code Contract](#exit-code-contract) and [Graceful Degradation](#graceful-degradation) above, as well as the project's [Design Philosophy](../CONTRIBUTING.md#design-philosophy).

### Integration Tiers

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

### Eligibility

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

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

### Maintenance

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

---

### Overview (`openclaw/README.md`)

# RTK Plugin for OpenClaw

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

## How it works

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

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

## Installation

### Prerequisites

RTK must be installed and available in `$PATH`:

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

### Install the plugin

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

# Restart the gateway
openclaw gateway restart
```

### Or install via OpenClaw CLI

```bash
openclaw plugins install ./openclaw
```

## Configuration

In `openclaw.json`:

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

## Permissions

RTK keeps the deny gate. OpenClaw owns approval.

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

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

What does **not** change:

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

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

### Writing exec rules

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

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

### Upgrading from an earlier build

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

### rtk version

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

## What gets rewritten

Everything that `rtk rewrite` supports (30+ commands). See the [full command list](https://github.com/rtk-ai/rtk#commands).

## 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`)

<h1 align="center">rtk-proxy</h1>

<p align="center">
  The local daemon that extends the RTK engine to your agent's provider-API
  traffic — <b>compress, relay routed work, and more</b>.
</p>

<p align="center">
  <a href="./LICENSE"><img alt="License: BUSL-1.1" src="https://img.shields.io/badge/license-BUSL--1.1-blue"></a>
</p>

---

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

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

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

## User documentation

Setup, what the daemon compresses, its settings and endpoints live in the user guide, [`docs/guide/proxy/`](../docs/guide/proxy/overview.md), published at [rtk-ai.app/guide](https://www.rtk-ai.app/guide/proxy/overview):

| Page | Covers |
|---|---|
| [RTK Proxy](../docs/guide/proxy/overview.md) | install, `rtk claude`, `rtk init -g`, `--env`, binding, state, savings |
| [What the Proxy Compresses](../docs/guide/proxy/compression.md) | normalisation, cache alignment, your `proxy.request` rules |
| [Proxy Configuration](../docs/guide/proxy/configuration.md) | settings, provider dialects, HTTP endpoints, attribution headers |

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

## Building

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

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

Contributing to the daemon: see [`docs/contributing/`](../docs/contributing/TECHNICAL.md).

## License

[Business Source License 1.1](./LICENSE) — 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 **contact@rtk-ai.app**. 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](../../docs/guide/pipelines/overview.md#three-files-you-might-write).
Copy one to `~/.config/rtk/pipeline.toml` (or drop several into
`~/.config/rtk/pipelines/`) and run `rtk pipeline validate`.

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

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

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

---

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

# Google Antigravity Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

## Permission Evaluation Note

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

---

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

# Claude Code Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# Cline / Roo Code Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# Codex CLI Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

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

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

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

## History in workspace-write sandboxes

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

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

See the [Codex configuration reference](https://developers.openai.com/codex/config-reference/).

---

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

# GitHub Copilot Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# Cursor IDE Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# RTK Plugin for Hermes

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

## Installation

```bash
rtk init --agent hermes
```

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

## How it works

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

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

## Fail-open behavior

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

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

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

## Limitations

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

---

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

# Kilo Code Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# OpenCode Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

---

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

# Pi Hooks

> Part of [`hooks/`](../README.md)

## Design Intent

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

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

## Specifics

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

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

## Uninstall

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

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

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

---

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

# Mistral Vibe Hooks

> Part of [`hooks/`](../README.md)

## Specifics

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

## Notes

- 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/`](../README.md)

## Specifics

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

---

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

# Analytics

> See also [docs/contributing/TECHNICAL.md](../../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](../../CONTRIBUTING.md#design-philosophy) and [TOML vs Rust decision table](../../CONTRIBUTING.md#toml-vs-rust-which-one)).

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

## Execution Flow

The shared wrappers in [`core/runner.rs`](../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

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

**`RunOptions` builder:**

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

**Example — filtered command (recommended):**

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

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

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

**Streaming filters (line-by-line):**

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

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

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

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

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

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

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

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

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

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

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

struct MyHandler { error_count: usize }

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

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

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

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

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

struct MyFilter { state: State }

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

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

**Example — passthrough command (no filtering):**

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

**Example — manual execution (custom logic):**

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

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


## Cross-Command Dependencies

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

## Cross-Cutting Behavior Contracts

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

### Exit Code Propagation

All module `run()` functions return `Result<i32>` 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](../../CONTRIBUTING.md#toml-vs-rust-which-one).

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

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

### TOML filter (simple line-based filtering)

1. **Create filter** in [`src/filters/`](../filters/README.md)
2. **Add rewrite pattern** in `src/discover/rules.rs`
3. **Write tests** and **update docs**

---

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

# Core Infrastructure

> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview

## Scope

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

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

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

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

## TOML Filter Pipeline

The TOML DSL applies 8 stages in order:

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

Three-tier filter lookup (first match wins):
1. `.rtk/filters.toml` (project-local, requires `rtk trust`)
2. `~/.config/rtk/filters.toml` (user-global)
3. Built-in filters concatenated by `build.rs` at compile time

## Source File Comment Stripping

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

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

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

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

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

## Tracking Database Schema

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

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

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

## Config Sections

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

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

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

[telemetry]
enabled = true

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

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

## Shared Utilities (utils.rs)

Key functions available to all command modules:

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

## Argument Tokenizer (arg_tokenizer.rs)

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

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

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

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

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

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

### Dialects

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

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

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

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

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

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

## Consumer Contracts

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

### Command Construction (`shell`)

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

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

### Tracking (`TimedExecution`)

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

### Output recovery (`tee_and_hint` + recall store)

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

For truncation recovery on **success** (e.g. a list capped at 20 items), use `tee::force_tee_hint()` (multi-line blocks) or `tee::force_tee_tail_hint(content, slug, offset)` (flat lists). All three persist the full output to the content-addressed recall store ([`retriever.rs`](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](../../docs/contributing/TECHNICAL.md#32-hook-interception-command-rewriting)

## What This Module Does

This module has two jobs:

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

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

## How Command Rewriting Works

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

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

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

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

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

**Per-segment rewriting** — Each segment goes through:

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

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

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

## Shared Lexer Toolkit

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

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

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

## How History Analysis Works

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

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

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

## Env Prefix Handling

The `ENV_PREFIX` regex strips env variable assignments and `env` from the front of commands. `sudo` is deliberately not stripped, so sudo-prefixed commands stay unclassified and pass through unrewritten. It handles:
- Unquoted: `FOO=bar`
- Double-quoted with spaces: `FOO="bar baz"`
- Single-quoted: `FOO='bar baz'`
- Escaped quotes: `FOO="he said \"hello\""`
- Chained: `A="x y" B=1 env git status`

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

## Process Wrapper Handling

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

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

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

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

## Adding a New Rewrite Rule

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

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

---

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

# Built-in Filters

> See also [docs/contributing/TECHNICAL.md](../../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](../../CONTRIBUTING.md#design-philosophy)). For the full TOML-vs-Rust decision criteria, see [CONTRIBUTING.md](../../CONTRIBUTING.md#toml-vs-rust-which-one).

TOML works well for commands with **predictable, line-by-line text output** where regex filtering achieves 60%+ savings:
- Install/update logs (brew, composer, poetry) — strip `Using ...` / `Already installed` lines
- System monitoring (df, ps, systemctl) — keep essential rows, drop headers/decorations
- Simple linters (shellcheck, yamllint, hadolint) — strip context, keep findings
- Infra tools (terraform plan, helm, rsync) — strip progress, keep summary

For the full contribution checklist (including `discover/rules.rs` registration), see [src/cmds/README.md — Adding a New Command Filter](../cmds/README.md#adding-a-new-command-filter).

## Adding a filter

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

## File format

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

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

## Available filter fields

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

## Naming convention

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

## Build and runtime pipeline

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

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

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

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

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

    G --> H & J & L & R
```

## Filter lookup priority

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

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

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

## Custom filters and trust

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

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

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

```bash
rtk trust       # lists each detected filter (labelled project/global) + a risk summary, then asks to confirm ([y/N], or --yes)
rtk untrust     # revokes trust
```

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

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

---

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

# Hook System

> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview | [hooks/](../../hooks/README.md) for deployed hook artifacts

## Scope

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

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

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

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

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

## Installation Modes

`rtk init` supports these installation flows:

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


## Integrity Verification

The integrity system prevents unauthorized hook modifications:

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

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

## PatchMode Behavior

Controls how `rtk init` modifies agent settings files:

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

## Atomicity and Safety

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

## Permission Model

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

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

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

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

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

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

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

### Per-tool support

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

### Implementation

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

## Exit Code Contract

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

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

---

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

# Learn — CLI Correction Detection

> See also [docs/contributing/TECHNICAL.md](../../docs/contributing/TECHNICAL.md) for the full architecture overview

## Purpose

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

## Key Types

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

## Dependencies

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

## Detection Algorithm

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

---

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

# Parser Infrastructure

> See also [docs/contributing/TECHNICAL.md](../../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

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

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

## Canonical Types

### TestResult
For test runners (vitest, playwright, jest, etc.)
- Fields: `total`, `passed`, `failed`, `skipped`, `duration_ms`, `failures`
- Formatter: Shows summary + failure details (compact: top 5, verbose: all)

### LintResult
For linters (eslint, biome, tsc, etc.)
- Fields: `total_files`, `files_with_issues`, `total_issues`, `errors`, `warnings`, `issues`
- Formatter: Groups by rule_id, shows top violations

### DependencyState
For package managers (pnpm, npm, cargo, etc.)
- Fields: `total_packages`, `outdated_count`, `dependencies`
- Formatter: Shows upgrade paths (current → latest)

### BuildOutput
For build tools (next, webpack, vite, cargo, etc.)
- Fields: `success`, `duration_ms`, `bundles`, `routes`, `warnings`, `errors`
- Formatter: Shows bundle sizes, route metrics

## Format Modes

### Compact (default, verbosity=0)
- Summary only
- Top 5-10 items
- Token-optimized

### Verbose (verbosity=1)
- Full details
- All items (up to 20)
- Human-readable

### Ultra (verbosity=2+)
- Symbols: ✓✗⚠ pkg: ^
- Ultra-compressed
- 30-50% token reduction

## Error Handling

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

### Degradation Warnings

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

## Migration Guide

### Existing Module → Parser Trait

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

## Testing

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

## Benefits

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

## Roadmap

### Phase 4: Module Migration
- [ ] vitest_cmd.rs → VitestParser
- [ ] playwright_cmd.rs → PlaywrightParser
- [ ] pnpm_cmd.rs → PnpmParser (list, outdated)
- [ ] lint_cmd.rs → EslintParser
- [ ] tsc_cmd.rs → TscParser
- [ ] gh_cmd.rs → GhParser

### Phase 5: Observability
- [ ] Extend tracking.db: `parse_tier`, `format_mode`
- [ ] `rtk parse-health` command
- [ ] Alert if degradation > 10%

---

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

# Cloud and Infrastructure

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# .NET Ecosystem

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# Git and VCS

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

## Cross-command

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

## glab vs gh JSON schema quick-ref

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

---

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

# Go Ecosystem

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# JavaScript / TypeScript / Node

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

## Cross-command

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

---

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

# JVM ecosystem filters

Filters for JVM-based build tools.

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

## Maven (`mvn_cmd.rs`)

Phase routing (`detect_phase`):

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

Key behaviours:

- **ANSI strip first** in every filter — real Maven output contains colour escapes.
- **English-footer guard** — if neither `BUILD SUCCESS` nor `BUILD FAILURE` appears as a trimmed line suffix, return the ANSI-stripped raw input unchanged. Protects non-English locales.
- **Verbose bypass** — `-X`, `--debug`, `-e`, `--errors` skip filtering (`run_passthrough`). User asked for detail; respect it.
- **Surefire block collapse** — Surefire emits `[INFO] Running <FQN>` … `[INFO] Tests run: N, Failures: F, Errors: E, …, Time elapsed: T s - in <FQN>`. The filter buffers each block and emits it only when `F > 0` or `E > 0`. Passing blocks (the bulk of healthy-project output) are dropped silently. Failing blocks are emitted with framework stack frames stripped via a deny-list (`at org.junit.`, `at java.util.`, `at sun.reflect.`, etc.).
- **Multi-failure classes (trail re-arm)** — when a single class has several failing tests, Surefire 3.x emits one blank-separated detail block per failing test under a single close line. When a failure trail ends at a blank line, the state machine arms a re-entry: the next per-test subline (`[ERROR] FQN.method -- Time elapsed: … <<< FAILURE!` or `<<< ERROR!`) re-enters the trail with the same keep/drop decision, so every failure message survives (and a capped class drops *all* its blocks). Any other non-blank line disarms the re-entry.
- **`<<< ERROR!` markers** — per-test sublines use `<<< ERROR!` for thrown (non-assertion) exceptions; the close-line regex also tolerates an `ERROR!` marker defensively (Surefire 3.5.5 emits `FAILURE!` even for errors-only classes — failure detection keys off the `Failures`/`Errors` counts, not the marker).
- **Help-boilerplate stripping (all modes)** — the post-failure block Maven emits after `[ERROR] Failed to execute goal` (`See …`, `-> [Help 1]`, `Re-run Maven`, `To see the full stack trace`, `For more information`, help URLs, bare `[ERROR]` dividers) is dropped in quiet *and* non-quiet filters alike (shared `BOILER_PREFIXES`). Deliberately kept as signal: `Failed to execute goal` itself and the multi-module resume hint (`[ERROR] After correcting the problems…` + `[ERROR]   mvn <args> -rf :module` — tells the user/agent how to resume the build). Real durations (`Time elapsed: … s`, `Total time: …`) ship untouched — the numbers are diagnostic signal.
- **Wrapper detection** — `./mvnw` (POSIX) and `mvnw.cmd` (Windows) detected via string-literal `Command::new` (semgrep-safe); falls back to `resolved_command("mvn")`.
- **Maven Daemon (`rtk mvnd`)** — `mvnd` is a separate entry point (`run_daemon`), not a `mvn` alias: it shares phase detection and every filter, but always executes `mvnd` and is never substituted by a `./mvnw` wrapper found in the working directory. The daemon's rolling console UI only engages on a TTY, so what rtk captures is ordinary Maven log output and the existing filters apply unchanged. Two daemon-specific quirks are handled explicitly:
  - **`[INFO]`-prefixed blanks** — the daemon logger prefixes even empty lines with `[INFO] `, so the Surefire failure trail's blank-line terminator uses `is_blank_separator` (a bare `[INFO]` counts as blank). Without it the trail never closes and everything after the last failing class passes through verbatim.
  - **Parallel-reactor lanes** — `mvnd` builds multi-module reactors in parallel by default and prefixes per-module lines with `[module] `, interleaved line-by-line. `split_lane` classifies on the prefix-stripped view while emitting the original (module identity survives), and each module gets its own `SurefireLane` (block machine + compile-continuation flag + summary-open flag) so a passing close from one module can't be attributed to another's open block. Three consequences of the interleaving are handled explicitly: unprefixed raw lines (stack traces, javac `symbol:` / `location:` continuations) go to the lane in an active **failure trail** before any lane merely holding an open block (`raw_owner`) — otherwise a module opening a block between a failing close and its exception swallows the diagnostics and drops them when it closes green — and when ownership is genuinely ambiguous (several plain blocks open, no trail) the line is preserved verbatim rather than routed to a guess. The `[ERROR] Failures:` budget stays reactor-wide (`FailuresSummaryCap`, reset only when a header opens the first summary), so interleaved per-module summaries share one cap instead of each getting `MAX_MVN_FAILING_CLASSES`.

  One known limit remains, not a regression — it already applies to `mvn -T`: daemon status chatter (`Connecting to the daemon`, …) carries no `[INFO]`/`[ERROR]` prefix, so the keep-list filters (`filter_surefire`, `filter_compile`, `filter_package`) drop it — but `filter_quiet` has no keep-list, only a deliberate "keep anything else" safety net, so `rtk mvnd -q` passes that chatter through where `rtk mvn -q` emits nothing.
- **Reactor Summary preservation** — for multi-module builds, the trailing `Reactor Summary for <root>` block with per-module SUCCESS/FAILURE rows is kept (toggled by a `[INFO] Reactor Summary for ` header and cleared on `BUILD SUCCESS` / `BUILD FAILURE`).
- **Failure cap** — both the count of emitted failing test classes and the size of the `[ERROR] Failures:` summary block are bounded by `MAX_MVN_FAILING_CLASSES = CAP_WARNINGS` (the shared test-failure cap class from `src/core/truncate.rs`, same binding as pytest/rspec/rake/runner). Excess emissions are replaced by a single `… +N more failing test classes` / `… +N more failures` tail (canonical `join_with_overflow` shape) to keep large failure sets compact; the raw output stays recoverable via the tee `[full output: …]` hint. Per the core cap policy, a cap of `0` means summary-only: no blocks emitted, the tail still counts every dropped class.

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

### Integrity-check whitelist

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

## Gradle (`gradlew_cmd.rs`)

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

---

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

# PHP

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# Python Ecosystem

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

## Cross-command

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

---

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

# Ruby on Rails

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# Rust Ecosystem

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

---

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

# System and Generic Utilities

> Part of [`src/cmds/`](../README.md) — see also [docs/contributing/TECHNICAL.md](../../../docs/contributing/TECHNICAL.md)

## Specifics

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

## Cross-command

- `format_cmd` routes to `cmds/js/prettier_cmd` and `cmds/python/ruff_cmd`

---

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

---
title: RTK Documentation
description: RTK (Rust Token Killer) — cut the bash output your agent reads on common dev commands, with zero workflow changes
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 output
```

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

## What the savings mean

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

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

See [How RTK Savings Work](./resources/savings-explained.md) 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](./resources/what-rtk-covers.md) for the full list.

## Get started

1. **[Installation](./getting-started/installation.md)** — Install RTK and verify you have the right package
2. **[Quick Start](./getting-started/quick-start.md)** — Connect to your AI assistant in 5 minutes
3. **[Supported Agents](./getting-started/supported-agents.md)** — Claude Code, Cursor, Copilot, Gemini, and more

## Measure your savings

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

See [Token Savings Analytics](./analytics/gain.md) for export formats and analysis workflows.

## Analyze your usage

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

See [Discover and Session](./analytics/discover.md) for details.

## Go further

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

- **[The engine](./pipelines/overview.md)** — 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 on `rtk:default` and nothing changes. Start with [Writing a Pipeline](./pipelines/writing-pipelines.md); [Conditions and Judges](./pipelines/conditions.md) and [Methods](./pipelines/methods.md) are the vocabulary, [Pipeline Commands](./pipelines/commands.md) is for inspecting, debugging and sharing one.
- **[The RTK Proxy](./proxy/overview.md)** — 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](./proxy/compression.md) and [Proxy Configuration](./proxy/configuration.md).

## Further reading

- [Configuration](./getting-started/configuration.md) — config.toml, global flags, env vars, recall recovery
- [Troubleshooting](./resources/troubleshooting.md) — common issues and fixes
- [Telemetry & Privacy](./resources/telemetry.md) — what RTK collects and how to opt out
- [Agent Coverage](./resources/agent-coverage.md) — what each agent's hook API lets RTK do
- [Editions and Licensing](./resources/editions.md) — the three downloads, what each contains, where the licence boundary falls
- [ARCHITECTURE.md](https://github.com/rtk-ai/rtk/blob/master/ARCHITECTURE.md) — system design for contributors

---

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

---
title: Pipelines
description: Compose what RTK does with one pipeline.toml — upgrading is a no-op, everything beyond today is opt-in
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](../resources/editions.md).
:::

## 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](../proxy/overview.md) 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](./reference-pipelines.md) you opt into — `rtk init` offers the proxy one; `rtk:posthook` you add by hand, as below.

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

**Tune one thing.**

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

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

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

**Adopt a behaviour and supply what only you can.**

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

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

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

## Where to go next

| Page | What it covers |
|---|---|
| [Writing a Pipeline](./writing-pipelines.md) | where files live, the rule tree, actions, routing, overrides |
| [Conditions and Judges](./conditions.md) | every `when` key, the call context, and AI classifiers (judges) |
| [Methods](./methods.md) | the transforms rules chain together, your own chains and filters |
| [Directives](./directives.md) | text RTK puts in front of the agent |
| [Tasks and Upstreams](./tasks.md) | delegating work to a cheaper model |
| [Reference Pipelines](./reference-pipelines.md) | the six built-ins, as literal files |
| [Pipeline Commands](./commands.md) | show, validate, explain, shadow, share, adopt |
| [Errors and Warnings](./errors.md) | every diagnostic code, and the engine's limits |
| [Plugins](./plugins.md) | 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
description: Run the RTK Proxy daemon (rtk-proxy) and point your AI agent at it so the pipeline also runs on the requests it sends to the model
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 gain
```

It is local: one background process on your machine. It does four things — **intercept, compress, forward, record** — and does not store prompt or completion text (the opt-in [CCR](./compression.md#large-tool-results-ccr) keeps the originals it compresses). What it compresses is in [What the Proxy Compresses](./compression.md); its settings and endpoints are in [Proxy Configuration](./configuration.md).

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](../resources/editions.md).
:::

## Install the daemon

Download the `rtk-proxy-<target>` archive for your platform from the [releases page](https://github.com/rtk-ai/rtk/releases) and put the binary next to `rtk` (anywhere on your `PATH` also works).

From a source checkout:

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

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

## Quick start

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

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

You rarely need `start` by hand — `rtk claude` starts the daemon for you. See [Getting your agent onto the proxy](#getting-your-agent-onto-the-proxy).

## 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 <harness>` — the launcher

```bash
rtk claude
```

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

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

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

| Subcommand | Base-URL variable |
|---|---|
| `rtk claude` | `ANTHROPIC_BASE_URL` |

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

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

### `--env` — one shell, by hand

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

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

## Binding to another address

Start it on a different address with `--bind`:

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

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

## Runtime settings and background service

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

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

Run either with `--help` to see the daemon's own options; the settings themselves are listed in [Proxy Configuration](./configuration.md). `service` is choice 2 of the [guided setup](#rtk-init--g--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](./compression.md#large-tool-results-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](../analytics/gain.md#live-dashboard) and [Attribution](../analytics/gain.md#attribution).

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

---

### TELEMETRY (`docs/TELEMETRY.md`)

# Telemetry

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

## Data Collector

**Entity**: `RTK AI Labs`
**Contact**: contact@rtk-ai.app

## Why we collect telemetry

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

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

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

## How it works

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

**Source code**: [`src/core/telemetry.rs`](../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.

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

Environment variable override (blocks telemetry regardless of consent):
```bash
export RTK_TELEMETRY_DISABLED=1
```

## Retention Policy

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

## Your Rights (GDPR)

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

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

## Erasure Procedure

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

## Data Handling

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

### Server-side Requirements

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

## For contributors

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

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

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

---

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

# rtk Architecture Documentation

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

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

---

## Table of Contents

1. [System Overview](#system-overview)
2. [Command Lifecycle](#command-lifecycle)
3. [Module Organization](#module-organization)
4. [Filtering Strategies](#filtering-strategies)
5. [Shared Infrastructure](#shared-infrastructure)
6. [Token Tracking System](#token-tracking-system)
7. [Global Flags Architecture](#global-flags-architecture)
8. [Error Handling](#error-handling)
9. [Configuration System](#configuration-system)
10. [Common Patterns](#common-patterns)
11. [Build Optimizations](#build-optimizations)
12. [Extensibility Guide](#extensibility-guide)
13. [Architecture Decision Records](#architecture-decision-records)

---

## System Overview

> For the proxy pattern diagram and key components table, see [TECHNICAL.md](TECHNICAL.md#2-architecture-overview).

### Design Principles

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

### Hook Architecture (v0.9.5+)

> For the hook interception diagram and agent-specific JSON formats, see [TECHNICAL.md](TECHNICAL.md#32-hook-interception-command-rewriting) and [hooks/README.md](../../src/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 / auditing
```

---

## Command 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](TECHNICAL.md#4-folder-map) 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, err
```

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

### Module Breakdown

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

### Module Count Breakdown

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

---

## Filtering Strategies

### Strategy Matrix

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

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

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

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

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

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

**Detection**: File extension-based with fallback heuristics

---

## Python & Go Module Architecture

### Design Rationale

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

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

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

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

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

Mirrors: lint, prettier              Mirrors: git, cargo
```

### Python Stack Architecture

#### Command Implementations

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

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

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

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

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

pytest_cmd.rs     STATE MACHINE         Text parser       90%+

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

pip_cmd.rs        JSON PARSING          JSON API          70-85%

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

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

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

#### Shared Infrastructure

**No Package Manager Detection**
Unlike JS/TS modules, Python commands don't auto-detect poetry/pipenv/pip because:
- `pip` is universally available (system Python)
- `uv` detection is explicit (binary presence check)
- Poetry/pipenv aren't execution wrappers (they manage virtualenvs differently)

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

### Go Stack Architecture

#### Command Implementations

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

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

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

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

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

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

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

golangci_cmd.rs   JSON PARSING          JSON API          85%

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

#### Sub-Enum Pattern (go_cmd.rs)

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

**Why Sub-Enum?**
- `go test/build/vet` are semantically related (core Go toolchain)
- Mirrors existing git/cargo patterns (consistency)
- Natural CLI: `rtk go test` not `rtk gotest`

**Why golangci-lint Standalone?**
- Third-party tool (not core Go toolchain)
- Different output format (JSON API vs text)
- Distinct use case (comprehensive linting vs single-tool diagnostics)

### Ruby Module Architecture

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

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

```
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 build
```

### Module Integration Checklist

When adding Python/Go module support:

- [x] **Output Format**: JSON API > NDJSON > State Machine > Text Filters
- [x] **Failure Focus**: Hide passing tests, show failures only
- [x] **Exit Code Preservation**: Propagate tool exit codes for CI/CD
- [x] **Virtual Env Awareness**: Python modules respect active virtualenv
- [x] **Error Grouping**: Group by rule/file for linters (ruff, golangci-lint)
- [x] **Streaming Support**: Handle interleaved NDJSON events (go test)
- [x] **Verbosity Levels**: Support -v/-vv/-vvv for debug output
- [x] **Token Tracking**: Integrate with tracking::track()
- [x] **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](../../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](../../src/core/README.md#argument-tokenizer-arg_tokenizerrs).

### 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, pnpm
```

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

---

## Token Tracking System

### SQLite-Based Metrics

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

### Thread Safety

Single-threaded execution with `Mutex<Option<Tracker>>` 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](../../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)

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

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

#### 2. Verbosity Guards

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

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

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

---

## Build Optimizations

### Release Profile (Cargo.toml)

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

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

### Performance Targets

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

---

## Extensibility Guide

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

---

## Architecture Decision Records

### Why Rust?

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

### Why SQLite for Tracking?

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

### Why anyhow for Error Handling?

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

### Why Clap for CLI Parsing?

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

---

## Resources

- **[TECHNICAL.md](TECHNICAL.md)**: Guided tour of end-to-end flow
- **[CONTRIBUTING.md](../../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](../../CONTRIBUTING.md#design-philosophy) in `CONTRIBUTING.md`. Once you understand the mental model there, this guide describes the coding practices we use day-to-day in RTK and what reviewers will look for on your PR.

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

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

---

## Quick Start for Contributors

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

1. **Read the flow once.** Start at [`CONTRIBUTING.md`](../../CONTRIBUTING.md), then skim [`docs/contributing/TECHNICAL.md`](TECHNICAL.md) to see how a command flows from `main.rs` → a `*_cmd.rs` filter → tracking → stdout.
2. **Look at a good example.** [`src/cmds/git/git_cmd.rs`](../../src/cmds/git/git_cmd.rs) is a representative filter — it shows the `run()` entry point, `LazyLock` regex setup, filter helpers, and embedded tests all in one file.
3. **Know the shared helpers before reimplementing.** Two files cover most of what you need:
   - [`src/core/runner.rs`](../../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`](../../src/core/utils.rs) — shared utilities: `resolved_command()`, `strip_ansi()`, `truncate()`, `count_tokens()`, and more.
4. **Follow the checklist.** [`src/cmds/README.md — Adding a New Command Filter`](../../src/cmds/README.md#adding-a-new-command-filter) walks you through creating a filter, registering it, and adding tests.
5. **Write the test first.** We follow Red-Green-Refactor. A snapshot test plus a token-savings assertion (see [Testing](#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](../../CONTRIBUTING.md#design-philosophy) section in `CONTRIBUTING.md`.

Two practical reminders that come up often in review:

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

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

---

## Files, Functions, and Documentation

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

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

---

## Edge Cases

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

Referencing an issue is often the clearest form:

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

---

## Comments

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

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

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

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

---

## Variables

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

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

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

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

---

## Function and File Size

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

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

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

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

---

## Imports and Dependencies

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

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

---

## Error Handling

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

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

Example of the standard fallback pattern for a filter:

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

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

---

## Testing

See [`CONTRIBUTING.md` — Testing](../../CONTRIBUTING.md#testing) for the full strategy. In short, for a new filter you typically want:

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

Minimal example:

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

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

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

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

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

---

## Security

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

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

**Hook integrity.** RTK verifies hook files via SHA-256 hashes before operational commands. If a hook has been tampered with, RTK exits with code 1. See [`src/hooks/integrity.rs`](../../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`](../../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](../../CONTRIBUTING.md) — Design philosophy, PR process, branch naming, testing requirements
> - [ARCHITECTURE.md](ARCHITECTURE.md) — Deep reference: filtering taxonomy, performance targets, architecture decisions
> - Each folder has its own `README.md` with implementation details and file descriptions

---

## 1. Project Vision

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

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

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

---

## 2. Architecture Overview

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

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

---

## 3. End-to-End Flow

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

### 3.1 Hook Installation (`rtk init`)

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

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

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

> **Details**: [`src/hooks/README.md`](../../src/hooks/README.md) covers all installation modes, configuration files, and the uninstall flow.

### 3.2 Hook Interception (Command Rewriting)

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

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

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

> **Details**: [`hooks/README.md`](../../hooks/README.md) covers each agent's JSON format, the rewrite registry, compound command handling, and the `RTK_DISABLED` override.

#### Rewrite Pipeline

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

```
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 -c` wrappers enter one level of inner rewriting. The parser hands back byte ranges instead of a decoded script, so the wrapper's own bytes are never re-quoted, and it reports the shell it matched so the script is attested under that shell's grammar rather than under bash's.
- **Pipe semantics**: Producers and intermediate stages of `|` remain raw. Only an argument-safe final stage whose rule has `pipeline_final_safe` may be rewritten; initially this is limited to ordinary `grep` and `rg` invocations. Search pattern-file forms (`-f`/`--file`) defer because they can consume pipeline stdin as configuration. `|&` is recognized separately and its complete pipeline stays raw.
- **Double env prefix handling**: `classify_command()` strips env prefixes to match the underlying command against rules. `rewrite_segment()` extracts the same prefix separately to re-prepend it to the rewritten command.
- **Fallback contract**: If any segment fails to match, it stays raw. `rewrite_command()` returns `None` only when zero segments were rewritten.

### 3.3 CLI Parsing and Routing

Once the rewritten command reaches RTK:

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

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

### 3.4 Filter Execution

RTK has two filter systems:

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

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

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

> **Details**: [`src/cmds/README.md`](../../src/cmds/README.md) covers the common pattern, ecosystem organization, cross-command dependencies, and how to add new filters.

### 3.5 Fallback Path

When Clap parsing fails (unknown command):

1. Guard: check if the command is an RTK meta-command (`gain`, `init`, etc.) -- if so, show Clap error
2. Look up TOML DSL filters via `toml_filter::find_matching_filter()`
3. If TOML match: capture stdout, apply filter pipeline, track savings
4. If no match: pure passthrough with `Stdio::inherit`, track as 0% output reduction

```
Command received
  -> Clap parse succeeds?
     -> Yes: Route to Rust filter module
     -> No:  run_fallback()
              -> TOML filter match?
                 -> Yes: Capture stdout, apply filter, track savings
                 -> No:  Passthrough (inherit stdio, track 0% reduction)
```

> **Details**: [`src/core/README.md`](../../src/core/README.md) covers the TOML filter engine, filter pipeline stages, and trust-gated project filters.

### 3.6 Token Tracking

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

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

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

> **Details**: [`src/analytics/README.md`](../../src/analytics/README.md) covers the analytics modules, and [`src/core/README.md`](../../src/core/README.md) covers the tracking database schema.

### 3.7 Output Recovery (recall store)

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

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

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

> **Details**: [`src/core/README.md`](../../src/core/README.md) covers the recall store, tee mode configuration, and the rotation strategy.

---

## 4. Folder Map

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

### `src/` — Rust source code

| Directory | What it does | What you'll find in its README |
|-----------|-------------|-------------------------------|
| `main.rs` | CLI entry point, `Commands` enum, routing match | _(no README — read the file directly)_ |
| [`core/`](../../src/core/README.md) | Shared infrastructure | Tracking DB schema, config system, tee recovery, TOML filter engine, utility functions |
| [`hooks/`](../../src/hooks/README.md) | Hook system | Installation flow (`rtk init`), integrity verification, rewrite command, trust model |
| [`analytics/`](../../src/analytics/README.md) | Token savings analytics | `rtk gain` dashboard, Claude Code economics, ccusage parsing |
| [`cmds/`](../../src/cmds/README.md) | **Command filters (9 ecosystems)** | Common filter pattern, cross-command routing, token savings table, **links to each ecosystem** |
| [`discover/`](../../src/discover/README.md) | History analysis + rewrite registry | Rewrite patterns, session providers, compound command splitting |
| [`learn/`](../../src/learn/README.md) | CLI correction detection | Error classification, correction pair detection, rule generation |
| [`parser/`](../../src/parser/README.md) | Parser infrastructure | Canonical types (TestResult, LintResult, etc.), 3-tier format modes, migration guide |
| [`filters/`](../../src/filters/README.md) | 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/`](../../hooks/README.md) | _(parent)_ | **All JSON formats**, rewrite registry overview, exit code contract, override controls |
| [`claude/`](../../hooks/claude/README.md) | Claude Code | Shell hook mechanism, `PreToolUse` JSON, test script |
| [`copilot/`](../../hooks/copilot/README.md) | GitHub Copilot | Rust binary hook, single `PreToolUse` schema shared by VS Code Chat and Copilot CLI |
| [`cursor/`](../../hooks/cursor/README.md) | Cursor IDE | Shell hook, empty JSON response requirement |
| [`cline/`](../../hooks/cline/README.md) | Cline / Roo Code | Rules file (prompt-level, no programmatic hook) |
| [`windsurf/`](../../hooks/windsurf/README.md) | Windsurf / Cascade | Rules file (workspace-scoped) |
| [`codex/`](../../hooks/codex/README.md) | OpenAI Codex CLI | Native `PreToolUse` processor, hooks.json registration, AGENTS.md awareness |
| [`opencode/`](../../hooks/opencode/README.md) | OpenCode | TypeScript plugin, in-place mutation (OpenCode 2.0 and 1.x >= 1.18.29) |

---

## 5. Hook System Summary

RTK supports the following LLM agents through hook integrations:

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

> **Details**: [`hooks/README.md`](../../hooks/README.md) has the full JSON schemas for each agent. [`src/hooks/README.md`](../../src/hooks/README.md) covers installation, integrity verification, and the rewrite command.

---

## 6. Filter Pipeline Summary

### Rust Filters (cmds/**)

Compiled filter modules for complex transformations.

> **Details**: [`src/cmds/README.md`](../../src/cmds/README.md) and each ecosystem subdirectory README.

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

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

> **Details**: [`src/core/README.md`](../../src/core/README.md) covers the TOML filter engine.

---

## 7. Performance Constraints

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

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

---

## 8. Testing

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

### How to Write Tests

**1. Create a fixture from real command output** (not synthetic data):
```bash
kubectl get pods > tests/fixtures/kubectl_pods_raw.txt
```

**2. Write your test in the same module file** (`#[cfg(test)] mod tests`):
```rust
#[test]
fn test_my_filter() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    assert!(output.contains("expected content"));
    assert!(!output.contains("noise line"));
}
```

**3. Verify the output reduction** (>=20% of the bash output required; `count_tokens` in tests and the `bytes / 4` estimator behind `rtk gain` are both approximations, reliable as ratios):
```rust
#[test]
fn test_my_filter_savings() {
    let input = include_str!("../tests/fixtures/my_cmd_raw.txt");
    let output = filter_my_cmd(input);
    let savings = 100.0 - (count_tokens(&output) as f64 / count_tokens(input) as f64 * 100.0);
    assert!(savings >= 20.0, "Expected >=20% savings, got {:.1}%", savings);
}
```

### Test Organization

```
tests/
├── fixtures/           # Real command output (never synthetic)
│   ├── git_log_raw.txt
│   ├── cargo_test_raw.txt
│   └── dotnet/         # Ecosystem-specific fixtures
└── integration_test.rs # Integration tests (#[ignore])
```

- **Unit tests**: `#[cfg(test)] mod tests` embedded in each module
- **Fixtures**: real command output in `tests/fixtures/`
- **Integration tests**: `#[ignore]` attribute, run with `cargo test --ignored`

> For testing requirements, pre-commit gate, and PR checklist, see [CONTRIBUTING.md — Testing](../../CONTRIBUTING.md#testing).

---

## 9. Future Improvements

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

---

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

# RTK Maintainers Application

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

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

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

---

## How to apply guide

#### ✅ Requirements

To apply, you should have:

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

---

### ✍️ How to Apply

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

---

### 👀 What to Expect

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

---

## What Maintainers Do

### 🌱 Ecosystem Maintainers

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

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

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

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

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

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

---

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

# RTK Token Savings Audit Guide

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

## Overview

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

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

## Quick Reference

```bash
# Default summary view
rtk gain

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

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

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

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

## Command Options

### Temporal Flags

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

### Classic Flags (still available)

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

### Reset Flag

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

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

### Export Formats

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

## Output Examples

All values below are illustrative.

### Daily Breakdown

```
📅 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

```json
{
  "summary": {
    "total_commands": 300,
    "total_input": 1500000,
    "total_output": 300000,
    "total_saved": 1200000,
    "avg_savings_pct": 80.0
  },
  "daily": [
    {
      "date": "2026-01-01",
      "commands": 100,
      "input_tokens": 500000,
      "output_tokens": 100000,
      "saved_tokens": 400000,
      "savings_pct": 80.0
    }
  ],
  "weekly": [...],
  "monthly": [...]
}
```

**Use cases:**
- API integration
- Custom dashboards
- Automated reporting
- Data pipeline ingestion

### CSV Export

```csv
# Daily Data
date,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2026-01-01,100,500000,100000,400000,80.0
2026-01-02,100,500000,100000,400000,80.0

# Weekly Data
week_start,week_end,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2025-12-28,2026-01-03,300,1500000,300000,1200000,80.0

# Monthly Data
month,commands,input_tokens,output_tokens,saved_tokens,savings_pct
2026-01,300,1500000,300000,1200000,80.0
```

**Use cases:**
- Excel analysis
- Python/R data science
- Google Sheets dashboards
- Matplotlib/seaborn plotting

## Analysis Workflows

### Weekly Progress Tracking

```bash
# Generate weekly report every Monday
rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv

# Compare this week vs last week
rtk gain --weekly | tail -3
```

### Monthly Cost Analysis

```bash
# Export monthly data for budget review
rtk gain --monthly --format json | jq '.monthly[] |
  {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}'
```

### Data Science Analysis

```python
import pandas as pd
import subprocess

# Get CSV data
result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'],
                       capture_output=True, text=True)

# Parse daily data
lines = result.stdout.split('\n')
daily_start = lines.index('# Daily Data') + 2
daily_end = lines.index('', daily_start)
daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end])))

# Plot savings trend
daily_df['date'] = pd.to_datetime(daily_df['date'])
daily_df.plot(x='date', y='savings_pct', kind='line')
```

### Excel Analysis

1. Export CSV: `rtk gain --all --format csv > rtk-data.csv`
2. Open in Excel
3. Create pivot tables:
   - Daily trends (line chart)
   - Weekly totals (bar chart)
   - Savings % distribution (histogram)

### Dashboard Creation

```bash
# Generate dashboard data daily via cron
0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json

# Serve with static site
cat > index.html <<'EOF'
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="savings"></canvas>
<script>
fetch('rtk-stats.json')
  .then(r => r.json())
  .then(data => {
    new Chart(document.getElementById('savings'), {
      type: 'line',
      data: {
        labels: data.daily.map(d => d.date),
        datasets: [{
          label: 'Daily Savings %',
          data: data.daily.map(d => d.savings_pct)
        }]
      }
    });
  });
</script>
EOF
```

## Understanding Token Savings

### Token Estimation

`rtk gain` estimates tokens as `bytes / 4`; RTK ships no tokenizer. The same estimator is applied to raw and filtered output, so the percentage is reliable; the absolute token counts are approximate and will not match your provider's billing.

### Savings Calculation

```
Input Tokens    = estimate_tokens(raw_command_output)
Output Tokens   = estimate_tokens(rtk_filtered_output)
Saved Tokens    = Input - Output
Savings %       = (Saved / Input) × 100
```

`Savings %` is a bash output byte ratio. Those bytes are one contributor to input tokens, and input tokens are only part of a bill that also counts output tokens.

## Database Management

### Inspect Raw Data

```bash
# Location
ls -lh ~/.local/share/rtk/history.db

# Schema
sqlite3 ~/.local/share/rtk/history.db ".schema"

# Recent records
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"

# Total database size
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT COUNT(*),
          SUM(saved_tokens) as total_saved,
          MIN(DATE(timestamp)) as first_record,
          MAX(DATE(timestamp)) as last_record
   FROM commands"
```

### Backup & Restore

```bash
# Backup
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db

# Restore
cp ~/backups/rtk-history-YYYYMMDD.db ~/.local/share/rtk/history.db

# Export for migration
sqlite3 ~/.local/share/rtk/history.db .dump > rtk-backup.sql
```

### Cleanup

```bash
# Manual cleanup (older than 90 days)
sqlite3 ~/.local/share/rtk/history.db \
  "DELETE FROM commands WHERE timestamp < datetime('now', '-90 days')"

# Reset all data
rm ~/.local/share/rtk/history.db
# Next rtk command will recreate database
```

## Integration Examples

### GitHub Actions CI/CD

```yaml
# .github/workflows/rtk-stats.yml
name: RTK Stats Report
on:
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday
jobs:
  stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install rtk
        run: cargo install --git https://github.com/rtk-ai/rtk rtk
      - name: Generate report
        run: |
          rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json
      - name: Commit stats
        run: |
          git add stats/
          git commit -m "Weekly rtk stats"
          git push
```

### Slack Bot

```python
import subprocess
import json
import requests

def send_rtk_stats():
    result = subprocess.run(['rtk', 'gain', '--format', 'json'],
                           capture_output=True, text=True)
    data = json.loads(result.stdout)

    message = f"""
    📊 *RTK Token Savings Report*

    Total Saved: {data['summary']['total_saved']:,} tokens
    Savings Rate: {data['summary']['avg_savings_pct']:.1f}%
    Commands: {data['summary']['total_commands']}
    """

    requests.post(SLACK_WEBHOOK_URL, json={'text': message})
```

## Troubleshooting

### No data showing

```bash
# Check if database exists
ls -lh ~/.local/share/rtk/history.db

# Check record count
sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands"

# Run a tracked command to generate data
rtk git status
```

### Export fails

```bash
# Check for pipe errors
rtk gain --format json 2>&1 | tee /tmp/rtk-debug.log | jq .
```

### Incorrect statistics

Token estimation is a heuristic. For precise measurements:

```bash
# Install tiktoken
pip install tiktoken

# Validate estimation
rtk git status > output.txt
python -c "
import tiktoken
enc = tiktoken.get_encoding('cl100k_base')
text = open('output.txt').read()
print(f'Actual tokens: {len(enc.encode(text))}')
print(f'rtk estimate: {len(text) // 4}')
"
```

## Best Practices

1. **Regular Exports**: `rtk gain --all --format json > monthly-$(date +%Y%m).json`
2. **Trend Analysis**: Compare week-over-week savings to identify optimization opportunities
3. **Command Profiling**: Use `--history` to see which commands save the most
4. **Backup Before Cleanup**: Always backup before manual database operations
5. **CI Integration**: Track savings across team in shared dashboards

## See Also

- [README.md](../../README.md) - Full rtk documentation
- [Token Savings Analytics](../guide/analytics/gain.md) - The full `rtk gain` surface
- [How RTK Savings Work](../guide/resources/savings-explained.md) - 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](../guide/resources/savings-explained.md)

---

## Table of contents

1. [Overview](#overview)
2. [Global flags](#global-flags)
3. [File commands](#file-commands)
4. [Git commands](#git-commands)
5. [GitHub CLI commands](#github-cli-commands)
6. [Test commands](#test-commands)
7. [Build and lint commands](#build-and-lint-commands)
8. [Formatting commands](#formatting-commands)
9. [Package managers](#package-managers)
10. [Containers and orchestration](#containers-and-orchestration)
11. [Data and network](#data-and-network)
12. [Cloud and databases](#cloud-and-databases)
13. [Stacked PRs (Graphite)](#stacked-prs-graphite)
14. [Analytics and tracking](#analytics-and-tracking)
15. [Hook system](#hook-system)
16. [Configuration](#configuration)
17. [Recall system (output recovery)](#recall-system)
18. [Telemetry](#telemetry)

---

## Overview

rtk acts as a proxy between an LLM (Claude Code, Gemini CLI, etc.) and system commands. Four filtering strategies are applied depending on the command type:

| Strategy | Description | Example |
|----------|-------------|---------|
| **Smart filtering** | Removes noise (comments, whitespace, boilerplate) | `ls -la` -> compact tree |
| **Grouping** | Aggregates by directory, error type, rule | Tests grouped by file |
| **Truncation** | Keeps relevant context, removes redundancy | Condensed diff |
| **Deduplication** | Merges repeated log lines with counters | `error x42` |

### Fallback mechanism

If rtk does not recognize a subcommand, it runs the raw command (passthrough) and records the event in the tracking database. This makes rtk **always safe** to use -- no command is ever blocked.

---

## Global flags

These flags apply to **all** subcommands:

| Flag | Short | Description |
|------|-------|-------------|
| `--verbose` | `-v` | Increase verbosity (-v, -vv, -vvv). Shows filtering details. |
| `--skip-env` | -- | Sets `SKIP_ENV_VALIDATION=1` for child processes (Next.js, tsc, lint, prisma). |

**Examples:**

```bash
rtk -v git status          # Compact status + filtering details on stderr
rtk -vvv cargo test        # Maximum verbosity (debug)
rtk --skip-env next build  # Disable Next.js env validation
```

---

## File commands

### `rtk ls` -- Directory listing

**Purpose:** Replaces `ls` and `tree` with token-optimized output.

**Syntax:**
```bash
rtk ls [args...]
```

All native `ls` flags are supported (`-l`, `-a`, `-h`, `-R`, etc.).

**Before / After:**
```
# ls -la (45 lines, ~800 tokens)           # rtk ls (12 lines, ~150 tokens)
drwxr-xr-x  15 user staff 480 ...          my-project/
-rw-r--r--   1 user staff 1234 ...          +-- src/ (8 files)
-rw-r--r--   1 user staff 567 ...           |   +-- main.rs
...40 more lines...                         +-- Cargo.toml
                                            +-- README.md
```

---

### `rtk tree` -- Directory tree

**Purpose:** Proxy to native `tree` with filtered output.

**Syntax:**
```bash
rtk tree [args...]
```

Supports all native `tree` flags (`-L`, `-d`, `-a`, etc.).

---

### `rtk read` -- File reading

**Purpose:** Replaces `cat`, `head`, `tail` with smart content filtering.

**Syntax:**
```bash
rtk read <file> [options]
rtk read - [options]          # Read from stdin
```

**Options:**

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--level` | `-l` | `minimal` | Filtering level: `none`, `minimal`, `aggressive` |
| `--max-lines` | `-m` | unlimited | Structural preview capped at N lines (signatures and imports, not the first N) |
| `--head-lines` | | unlimited | Keep only the first N lines, byte-exact |
| `--tail-lines` | | unlimited | Keep only the last N lines, byte-exact |
| `--line-numbers` | `-n` | no | Show line numbers |

**Filtering levels:**

| Level | Description |
|-------|-------------|
| `none` | No filtering, raw output |
| `minimal` | Removes comments and excessive blank lines |
| `aggressive` | Signatures only (removes function bodies) |

**Before / After (aggressive mode):**
```
# cat main.rs (~200 lines)                  # rtk read main.rs -l aggressive (~50 lines)
fn main() -> Result<()> {                   fn main() -> Result<()> { ... }
    let config = Config::load()?;           fn process_data(input: &str) -> Vec<u8> { ... }
    let data = process_data(&input);        struct Config { ... }
    for item in data {                      impl Config { fn load() -> Result<Self> { ... } }
        println!("{}", item);
    }
    Ok(())
}
...
```

**Languages supported for filtering:** Rust, Python, JavaScript, TypeScript, Go, C, C++, Java, Ruby, Shell.

---

### `rtk smart` -- Heuristic summary

**Purpose:** Generates a 2-line technical summary of a source file.

**Syntax:**
```bash
rtk smart <file> [--model heuristic] [--force-download]
```

**Example:**
```
$ 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:**
```bash
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:**
```bash
rtk grep <pattern> [path] [options]
```

**Options:**

| Option | Short | Default | Description |
|--------|-------|---------|-------------|
| `--max-len` |  | 80 | Maximum line length |
| `--max` |  | 200 | Maximum number of results |
| `--context-only` |  | no | Show only the match context |

None of these options has a short form: `-l`, `-m` and `-c` are reserved for the native flags with the same letter (`--files-with-matches`, `--max-count`, `--count`) and are passed to the engine.

Extra arguments are passed to the engine actually invoked -- `grep` for `rtk grep`, `rg` for `rtk rg`; RTK never substitutes one for the other. A ripgrep-specific flag (`--glob`, `-t`) is therefore only valid under `rtk rg`. Flags that change the output format (`-c`, `-l`, `-L`, `-o`, `-Z`) go straight to the engine without RTK filtering.

There is no `--file-type` option: `rtk rg -t rust` filters by type, while `rtk grep -t rust` surfaces grep's error (`invalid option -- 't'`).

**Before / After:**
```
# 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:**
```bash
rtk ast-grep run -p '<pattern>' [path] [options]
```

Groups matches by file, capped at 5 per file and 50 in total; the overflow is replaced by a count note ("N more match line(s) in X" / "N more match line(s) in M more file(s) not shown"), followed by a `[full output: ...]` hint pointing to the complete output, as long as recall is enabled (`[retriever] mode`) and the compaction saves enough to pay for the hint. Otherwise the count note is followed by a fallback (`--json`, a narrower pattern, or `rtk proxy ast-grep`), and the full output is not archived. When the compacted output would end up costing more than the raw one (count notes that cost more than the lines they replace, or a hint or fallback longer than the margin saved), the raw output is printed whole, with no note and no fallback. ast-grep prints one line per source line of a match, and a structural match can span several lines, so the count is in lines, not matches. On a real search in this repository, ~85% reduction.

Only `run` is filtered: either named explicitly, or implied when no positional argument before `--` names another subcommand. `scan`, `test`, `new`, `lsp`, `outline`, `completions` and `help` pass through unchanged (the ast-grep 0.45.3 subcommands other than `run`): their output does not have this shape, and `lsp` talks over stdin. So do `run --stdin` and `run -i`: the first reads the source on the pipe the capture closes, the second opens a full-screen session (one question per match) that it writes to stdout while reading answers from /dev/tty, so the capture would swallow the whole session, question included, while ast-grep waits for an answer.

`--json` is not filtered -- an explicit request for structured output passes through unchanged, without compression.

---

### `rtk diff` -- Condensed diff

**Purpose:** Ultra-condensed diff between two files (changed lines only).

**Syntax:**
```bash
rtk diff <file1> <file2>
rtk diff -                    # Condense a unified diff read on stdin
```

Pour comparer deux fichiers : code de sortie **0** si identiques, **1** si differents,
**2** si un fichier ne peut pas etre lu. Les fichiers non UTF-8 sont compares octet
par octet ; seuls leurs noms sont affiches lorsqu'ils different.

---

### `rtk wc` -- Compact count

**Purpose:** Replaces `wc` with compact output (removes paths and padding).

**Syntax:**
```bash
rtk wc [args...]
```

Supports all native `wc` flags (`-l`, `-w`, `-c`, etc.).

---

## Git commands

### Overview

All git subcommands are supported. Unrecognized commands are passed directly to git (passthrough).

**Global git options:**

| Option | Description |
|--------|-------------|
| `-C <path>` | Change directory before running |
| `-c <key=value>` | Override a git config value |
| `--git-dir <path>` | Path to the .git directory |
| `--work-tree <path>` | Path to the working tree |
| `--no-pager` | Disable the pager |
| `--no-optional-locks` | Skip optional locks |
| `--bare` | Treat as a bare repository |
| `--literal-pathspecs` | Literal pathspecs |

---

### `rtk git status` -- Compact status

```bash
rtk git status [args...]    # Supports all git status flags
```

**Before / After:**
```
# git status (~20 lines, ~400 tokens)       # rtk git status (~5 lines, ~80 tokens)
On branch main                               main | 3M 1? 1A
Your branch is up to date with               M src/main.rs
  'origin/main'.                              M src/git.rs
                                              M tests/test_git.rs
Changes not staged for commit:                ? new_file.txt
  (use "git add <file>..." to update)        A staged_file.rs
  modified:   src/main.rs
  modified:   src/git.rs
  ...
```

---

### `rtk git log` -- Compact history

```bash
rtk git log [args...]    # Supports --oneline, --graph, --all, -n, etc.
```

**Before / After:**
```
# git log (50+ lines)                       # rtk git log -n 5 (5 lines)
commit abc123def... (HEAD -> main)           abc123 Fix token counting bug
Author: User <user@email.com>               def456 Add vitest support
Date:   Mon Jan 15 10:30:00 2024            789abc Refactor filter engine
                                             012def Update README
    Fix token counting bug                   345ghi Initial commit
...
```

---

### `rtk git diff` -- Compact diff

```bash
rtk git diff [args...]    # Supports --stat, --cached, --staged, etc.
```

**Before / After:**

Raw `git diff`:
```
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 -2
```

On a diff this small, rtk saves little. Compression comes from larger diffs, where per-file headers and per-hunk caps matter; truncated output ends with a truncation note and the hint `[full diff: rtk git diff --no-compact]`.

Hunk lines and `@@` headers are printed at column 0, in git's unified form, so `rtk git diff | grep "^-"` works. rtk's own annotations are indented by two spaces so those same greps do not count them: the per-file total (`  +2 -2`) and the truncation note (`  ... (30 deletions, 20 additions truncated)`).

Limitations:
- Beyond 100 changed lines per hunk, `grep -c "^-"` is capped at what is displayed; the truncation note gives the remainder per sign, but it is indented and therefore invisible to the same grep.
- On a combined diff (`diff --cc`, which git produces for each conflicted path during a merge, rebase, cherry-pick or stash pop), the marker can sit in the second column: ` +ours` is counted in the `+N -M` total but stays invisible to `grep "^+"`.
- The output is not an applicable patch: rtk removes the `diff --git` / `---` / `+++` headers. Use `rtk bypass git diff` for a faithful patch, or `rtk gh pr diff --patch`, which passes through unfiltered.

---

### `rtk git show` -- Compact show

```bash
rtk git show [args...]
```

Shows the commit summary + stat + compact diff.

> **Warning (redirecting to a file).** For a large blob
> (`rtk git show HEAD:large-file`), the output is windowed: only a preview
> is shown, followed by a hint
> `[see remaining: rtk proxy git show 'HEAD:...' | tail -n +N]`.
> A hand-written `rtk git show HEAD:x > file` can therefore silently
> truncate the content (the exit code stays 0). To capture the full
> file, follow the recovery hint, or use `rtk proxy git show`. A bare
> `git show` is not enough when the RTK hook is active: it is rewritten to
> `rtk git show`, which windows the output again.

---

### `rtk git add` -- Compact add

```bash
rtk git add [args...]    # Supports -A, -p, --all, etc.
```

**Output:** `ok` (a single word)

---

### `rtk git commit` -- Compact commit

```bash
rtk git commit -m "message" [args...]    # Supports -a, --amend, --allow-empty, etc.
```

**Output:** `ok abc1234` (confirmation + short hash)

---

### `rtk git push` -- Compact push

```bash
rtk git push [args...]    # Supports -u, remote, branch, etc.
```

**Before / After:**
```
# git push (15 lines, ~200 tokens)          # rtk git push (1 line, ~10 tokens)
Enumerating objects: 5, done.                ok main
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
...
```

---

### `rtk git pull` -- Compact pull

```bash
rtk git pull [args...]
```

**Output:** `ok 3 files +10 -2`

---

### `rtk git branch` -- Compact branches

```bash
rtk git branch [args...]    # Supports -d, -D, -m, etc.
```

Shows the current branch, local branches and remote branches compactly.

---

### `rtk git fetch` -- Compact fetch

```bash
rtk git fetch [args...]
```

**Output:** `ok fetched (N new refs)`

---

### `rtk git stash` -- Compact stash

```bash
rtk git stash [list|show|pop|apply|drop|push] [args...]
```

---

### `rtk git worktree` -- Compact worktree

```bash
rtk git worktree [add|remove|prune|list] [args...]
```

---

### Git passthrough

Any git subcommand not listed above is run directly:

```bash
rtk git rebase main        # Runs git rebase main
rtk git cherry-pick abc    # Runs git cherry-pick abc
rtk git tag v1.0.0         # Runs git tag v1.0.0
```

---

## GitHub CLI commands

### `rtk gh` -- Compact GitHub CLI

**Purpose:** Replaces `gh` with optimized output.

**Syntax:**
```bash
rtk gh <subcommand> [args...]
```

**Supported subcommands:**

| Command | Description |
|---------|-------------|
| `rtk gh pr list` | Compact PR list |
| `rtk gh pr view <num>` | PR details + checks |
| `rtk gh pr checks` | CI check status |
| `rtk gh issue list` | Compact issue list |
| `rtk gh run list` | Workflow run status |
| `rtk gh api <endpoint>` | Compact API response |

**Before / After:**
```
# gh pr list (~30 lines)                    # rtk gh pr list (~10 lines)
Showing 10 of 15 pull requests in org/repo   #42 feat: add vitest (open, 2d)
                                              #41 fix: git diff crash (open, 3d)
#42  feat: add vitest support                 #40 chore: update deps (merged, 5d)
  user opened about 2 days ago                #39 docs: add guide (merged, 1w)
  ... labels: enhancement
...
```

---

## Test commands

### `rtk test` -- Generic test wrapper

**Purpose:** Runs any test command and shows only the failures.

**Syntax:**
```bash
rtk test <command...>
rtk test --shell fish '<fish command>'
```

By default the command and its arguments are run directly, with no shell
expansion. `--shell` takes a whole command as a single argument when a shell's
syntax is needed.

**Example:**
```bash
rtk test cargo test
rtk test npm test
rtk test bun test
rtk test pytest
```

**Before / After:**
```
# cargo test (200+ lines on failure)        # rtk test cargo test (~20 lines)
running 15 tests                             FAILED: 2/15 tests
test utils::test_parse ... ok                  test_edge_case: assertion failed
test utils::test_format ... ok                 test_overflow: panic at utils.rs:18
test utils::test_edge_case ... FAILED
...150 lines of backtrace...
```

---

### `rtk err` -- Errors/warnings only

**Purpose:** Runs a command and shows only errors and warnings.

**Syntax:**
```bash
rtk err <command...>
rtk err --shell fish '<fish command>'
```

Without `--shell`, argument boundaries are preserved, and globs, variables and
operators are not interpreted by a shell.

**Example:**
```bash
rtk err npm run build
rtk err cargo build
```

---

### `rtk cargo test` -- Rust tests

```bash
rtk cargo test [args...]
```

Shows only failures. Supports all `cargo test` arguments.

---

### `rtk cargo nextest` -- Rust tests (nextest)

```bash
rtk cargo nextest [run|list|--lib] [args...]
```

Filters `cargo nextest` output to show only failures.

---

### `rtk jest` / `rtk vitest` -- Jest/Vitest tests

```bash
rtk jest [args...]
rtk vitest [args...]
```

---

### `rtk playwright test` -- Playwright E2E tests

```bash
rtk playwright [args...]
```

---

### `rtk pytest` -- Python tests

```bash
rtk pytest [args...]
```

---

### `rtk go test` -- Go tests

```bash
rtk go test [args...]
```

Uses Go's NDJSON streaming output for precise filtering.

---

## Build and lint commands

### `rtk cargo build` -- Rust build

```bash
rtk cargo build [args...]
```

Removes "Compiling..." lines, keeping only errors and the final result.

---

### `rtk cargo check` -- Rust check

```bash
rtk cargo check [args...]
```

Removes "Checking..." lines, keeping only errors.

---

### `rtk cargo clippy` -- Rust Clippy

```bash
rtk cargo clippy [args...]
```

Groups warnings by lint rule.

---

### `rtk cargo install` -- Rust install

```bash
rtk cargo install [args...]
```

Removes dependency compilation, keeping only the install result and errors.

---

### `rtk tsc` -- TypeScript compiler

```bash
rtk tsc [args...]
```

Groups TypeScript errors by file and error code.

**Before / After:**
```
# tsc --noEmit (50 lines)                   # rtk tsc (15 lines)
src/api.ts(12,5): error TS2345: ...          src/api.ts (3 errors)
src/api.ts(15,10): error TS2345: ...           TS2345: Argument type mismatch (x2)
src/api.ts(20,3): error TS7006: ...            TS7006: Parameter implicitly has 'any'
src/utils.ts(5,1): error TS2304: ...         src/utils.ts (1 error)
...                                            TS2304: Cannot find name 'foo'
```

---

### `rtk lint` -- ESLint / Biome

```bash
rtk lint [args...]
rtk lint biome [args...]
```

Groups violations by rule and by file. Auto-detects the linter.

---

### `rtk prettier` -- Format checking

```bash
rtk prettier [args...]    # e.g. rtk prettier --check .
```

Shows only the files that need formatting.

---

### `rtk format` -- Universal formatter

```bash
rtk format [args...]
```

Auto-detects the project formatter (prettier, black, ruff format) and applies a compact filter.

---

### `rtk next build` -- Next.js build

```bash
rtk next [args...]
```

Compact output with route metrics.

---

### `rtk ruff` -- Python linter/formatter

```bash
rtk ruff check [args...]
rtk ruff format --check [args...]
```

Compressed JSON output.

---

### `rtk mypy` -- Python type checker

```bash
rtk mypy [args...]
```

Groups type errors by file.

---

### `rtk golangci-lint` -- Go linter

```bash
rtk golangci-lint run [args...]
```

Compressed JSON output.

---

## Formatting commands

### `rtk prettier` -- Prettier

```bash
rtk prettier --check .
rtk prettier --write src/
```

---

### `rtk format` -- Universal detector

```bash
rtk format [args...]
```

Automatically detects prettier, black, ruff format and rustfmt. Applies a unified compact filter.

---

## Package managers

### `rtk pnpm` -- pnpm

| Command | Description |
|---------|-------------|
| `rtk pnpm list [-d N]` | Compact dependency tree |
| `rtk pnpm outdated` | Outdated packages: `pkg: old -> new` |
| `rtk pnpm install` | Filters progress bars |
| `rtk pnpm build` | Delegates to the Next.js filter |
| `rtk pnpm typecheck` | Delegates to the tsc filter |

Unrecognized subcommands are passed directly to pnpm (passthrough).

---

### `rtk npm` -- npm

```bash
rtk npm [args...]    # e.g. rtk npm run build
```

Filters npm boilerplate (progress bars, headers, etc.).

---

### `rtk npx` -- npx with smart routing

```bash
rtk npx [args...]
```

Routes to specialized filters:
- `rtk npx tsc` -> tsc filter
- `rtk npx eslint` -> lint filter
- `rtk npx prisma` -> prisma filter
- Others -> filtered passthrough

---

### `rtk pip` -- pip / uv

```bash
rtk pip list              # Package list (auto-detects uv)
rtk pip outdated          # Outdated packages
rtk pip install <pkg>     # Install
```

Auto-detects `uv` when available and uses it instead of `pip`.

---

### `rtk deps` -- Dependency summary

**Purpose:** Compact summary of project dependencies.

```bash
rtk deps [path]    # Default: current directory
```

Auto-detects `Cargo.toml`, `package.json`, `pyproject.toml`, `go.mod`, `Gemfile`, etc.

---

### `rtk prisma` -- Prisma ORM

| Command | Description |
|---------|-------------|
| `rtk prisma generate` | Client generation (removes ASCII art) |
| `rtk prisma migrate dev [--name N]` | Create and apply a migration |
| `rtk prisma migrate status` | Migration status |
| `rtk prisma migrate deploy` | Deploy to production |
| `rtk prisma db-push` | Schema push |

---

## Containers and orchestration

### `rtk docker` -- Docker

| Command | Description |
|---------|-------------|
| `rtk docker ps` | Compact container list |
| `rtk docker images` | Compact image list |
| `rtk docker logs <container>` | Deduplicated logs |
| `rtk docker compose ps` | Compact Compose services |
| `rtk docker compose logs [service]` | Deduplicated Compose logs |
| `rtk docker compose build [service]` | Build summary |

Unrecognized subcommands are passed through directly (passthrough).

**Before / After:**
```
# 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 <pod> [-c container]` | Deduplicated logs | Specific container |

Unrecognized subcommands are passed through directly (passthrough).

---

## Data and network

### `rtk json` -- JSON structure

**Purpose:** Shows the structure of a JSON file without the values.

```bash
rtk json <file> [--depth N]    # Default: depth 5
rtk json -                     # From stdin
```

**Before / After:**
```
# cat package.json (50 lines)               # rtk json package.json (10 lines)
{                                            {
  "name": "my-app",                            name: string
  "version": "1.0.0",                         version: string
  "dependencies": {                            dependencies: { 15 keys }
    "react": "^18.2.0",                        devDependencies: { 8 keys }
    "next": "^14.0.0",                         scripts: { 6 keys }
    ...15 dependencies...                    }
  },
  ...
}
```

---

### `rtk env` -- Environment variables

```bash
rtk env                    # All variables (sensitive ones masked)
rtk env -f AWS             # Filter by name
rtk env --show-all         # Include sensitive values
```

Sensitive variables (tokens, secrets, passwords) are masked by default: `AWS_SECRET_ACCESS_KEY=***`.

---

### `rtk log` -- Deduplicated logs

**Purpose:** Filters and deduplicates log output.

```bash
rtk log <file>     # From a file
rtk log            # From stdin (pipe)
```

Repeated lines are merged: `[ERROR] Connection refused (x42)`.

---

### `rtk curl` -- HTTP with truncation

```bash
rtk curl [args...]
```

Truncates long responses and saves the full output for recovery.

---

### `rtk wget` -- Compact download

```bash
rtk wget <url> [args...]
rtk wget -O - <url>           # Output to stdout
```

Removes progress bars and noise.

---

### `rtk summary` -- Heuristic summary

**Purpose:** Runs a command and generates a heuristic summary of its output.

```bash
rtk summary <command...>
rtk summary --shell fish '<fish command>'
```

Useful for long-running commands whose output has no dedicated filter.
The command is run directly by default; `--shell` explicitly turns on
interpretation of a whole command by the chosen shell.

---

### `rtk bypass` -- Passthrough with tracking

**Purpose:** Runs a command **without filtering** but records usage for tracking.

```bash
rtk bypass <command...>
```

Useful for debugging: compare raw output with filtered output.

---

## Cloud and databases

### `rtk aws` -- AWS CLI

```bash
rtk aws <service> [args...]
```

Forces JSON output and compresses the result. Supports all AWS services (sts, s3, ec2, ecs, rds, cloudformation, etc.).

---

### `rtk psql` -- PostgreSQL

```bash
rtk psql [args...]
```

Removes table borders and compresses the output.

---

## Stacked PRs (Graphite)

### `rtk gt` -- Graphite

| Command | Description |
|---------|-------------|
| `rtk gt log` | Compact stack log |
| `rtk gt submit` | Compact submit |
| `rtk gt sync` | Compact sync |
| `rtk gt restack` | Compact restack |
| `rtk gt create` | Compact create |
| `rtk gt branch` | Compact branch info |

Unrecognized subcommands are passed through directly or detected as git passthrough.

---

## Analytics and tracking

### Tracking system

RTK records every command execution in a SQLite database:

- **Location:** `~/.local/share/rtk/tracking.db` (Linux), `~/Library/Application Support/rtk/tracking.db` (macOS)
- **Retention:** 90 days, automatic
- **Metrics:** input/output tokens, bash output reduction percentage, execution time, project

---

### `rtk gain` -- Bash output reduction statistics

```bash
rtk gain                        # Global summary
rtk gain -p                     # Filter by current project
rtk gain --graph                # ASCII graph (last 30 days)
rtk gain --history              # Recent command history
rtk gain --daily                # Day-by-day breakdown
rtk gain --weekly               # Weekly breakdown
rtk gain --monthly              # Monthly breakdown
rtk gain --all                  # All breakdowns
rtk gain --quota -t pro         # Estimate of bash output saved against the monthly quota
rtk gain --failures             # Parsing failure log (commands that fell back)
rtk gain --format json          # JSON export (for dashboards)
rtk gain --format csv           # CSV export
```

**Options:**

| Option | Short | Description |
|--------|-------|-------------|
| `--project` | `-p` | Filter by current directory |
| `--graph` | `-g` | ASCII graph of the last 30 days |
| `--history` | `-H` | Recent command history |
| `--quota` | `-q` | Estimate of bash output saved against the monthly quota |
| `--tier` | `-t` | Subscription tier: `pro`, `5x`, `20x` (default: `20x`) |
| `--daily` | `-d` | Daily breakdown |
| `--weekly` | `-w` | Weekly breakdown |
| `--monthly` | `-m` | Monthly breakdown |
| `--all` | `-a` | All breakdowns |
| `--format` | `-f` | Output format: `text`, `json`, `csv` |
| `--failures` | `-F` | Show commands that fell back |

**Example output (illustrative values):**
```
$ rtk gain
RTK Token Savings Summary
  Total commands:     1,000
  Total input:        2,000,000 tokens
  Total output:       400,000 tokens
  Total saved:        1,600,000 tokens (80%)
  Avg per command:    1,600 tokens saved

Top commands:
  git status    300x  -80%
  cargo test    150x  -90%
  git diff      100x  -75%
```

---

### `rtk discover` -- Missed opportunities

**Purpose:** Analyzes Claude Code history to find commands that rtk could have optimized.

```bash
rtk discover                          # Current project, last 30 days
rtk discover --all --since 7          # All projects, last 7 days
rtk discover -p /path/to/project      # Filter by project
rtk discover --limit 20               # Max commands per section
rtk discover --format json            # JSON export
```

**Options:**

| Option | Short | Description |
|--------|-------|-------------|
| `--project` | `-p` | Filter by project path |
| `--limit` | `-l` | Max commands per section (default: 15) |
| `--all` | `-a` | Scan all projects |
| `--since` | `-s` | Last N days (default: 30) |
| `--format` | `-f` | Format: `text`, `json` |

---

### `rtk learn` -- Learn from errors

**Purpose:** Analyzes Claude Code CLI error history to detect recurring corrections.

```bash
rtk learn                             # Current project
rtk learn --all --since 7             # All projects
rtk learn --write-rules               # Generate .claude/rules/cli-corrections.md
rtk learn --min-confidence 0.8        # Confidence threshold (default: 0.6)
rtk learn --min-occurrences 3         # Minimum occurrences (default: 1)
rtk learn --format json               # JSON export
```

---

### `rtk cc-economics` -- Claude Code economics

**Purpose:** Compares Claude Code spending (via ccusage) with the bash output saved by RTK.

```bash
rtk cc-economics                      # Summary
rtk cc-economics --daily              # Daily breakdown
rtk cc-economics --weekly             # Weekly breakdown
rtk cc-economics --monthly            # Monthly breakdown
rtk cc-economics --all                # All breakdowns
rtk cc-economics --format json        # JSON export
```

---

### `rtk hook-audit` -- Hook metrics

**Prerequisite:** Requires `RTK_HOOK_AUDIT=1` in the environment.

```bash
rtk hook-audit                        # Last 7 days (default)
rtk hook-audit --since 30             # Last 30 days
rtk hook-audit --since 0              # Full history
```

---

## Hook system

### How it works

The RTK hook intercepts Bash commands in Claude Code **before they run** and automatically rewrites them to their RTK equivalent.

**Flow:**
```
Claude Code "git status"
    |
    v
settings.json -> PreToolUse hook
    |
    v
rtk-rewrite.sh (bash)
    |
    v
rtk rewrite "git status"  ->  "rtk git status"
    |
    v
Claude Code runs "rtk git status"
    |
    v
Filtered output returned to Claude
```

**Key points:**
- Claude never sees the rewrite -- it simply receives optimized output
- The hook is a lightweight delegator that calls `rtk rewrite`
- Commands already prefixed with `rtk` pass through unchanged
- Heredocs (`<<`) are not modified
- Unrecognized commands pass through unchanged

### Installation

```bash
rtk init -g                     # Recommended install (hook + RTK.md)
rtk init -g --auto-patch        # Non-interactive (CI/CD)
rtk init -g --hook-only         # Hook only, without RTK.md
rtk init --show                 # Check the installation
rtk init -g --uninstall         # Uninstall
```

### Installed files

| File | Description |
|------|-------------|
| `~/.claude/hooks/rtk-rewrite.sh` | Hook script (delegates to `rtk rewrite`) |
| `~/.claude/RTK.md` | Minimal instructions for the LLM |
| `~/.claude/settings.json` | PreToolUse hook registration |

### `rtk rewrite` -- Command rewriting

Internal command used by the hooks. The exit code carries the permission decision, taken from Claude Code's permission rules applied to every command in a chain:

- `0`: prints the rewritten command; allow rules cover every command, so the hook may allow it without asking
- `1`: no output, no rewrite (no RTK equivalent, nothing to rewrite, a command excluded in config, a construct RTK does not rewrite such as `$(...)` or a file redirect, or a deny rule matched inside a shell wrapper's script); the command runs unchanged
- `2`: a deny matched (checked before anything else). With no output, a deny rule matched and the hook defers to the agent's own deny. With output, an rtk pipeline rule denied or routed the command, and the hook blocks it with that message
- `3`: prints the rewritten command; an ask rule matched, or allow rules do not cover every command, including when there are no rules at all; the agent asks for confirmation

An integration therefore reads stdout on `0` and `3` (and on `2`, for the block message), and keeps the confirmation prompt on `3`. With `RTK_REWRITE_HOST=openclaw`, an integration that handles approval itself gets `0` in place of every `3` that does not come from an ask rule; an integration that allows without asking on `0` therefore runs `rtk rewrite` without that variable.

```bash
rtk rewrite "git status"           # -> "rtk git status" (exit 3 with no rules)
rtk rewrite "echo hello"           # -> (exit 1, no rewrite)
rtk rewrite "rtk git status"       # -> "rtk git status" (exit 3 with no rules, unchanged)
```

### `rtk verify` -- Integrity check

Checks the integrity of the installed hook with a SHA-256 check.

```bash
rtk verify
```

### Automatically rewritten commands

| Raw command | Rewritten to |
|-------------|--------------|
| `git status/diff/log/add/commit/push/pull` | `rtk git ...` |
| `gh pr/issue/run` | `rtk gh ...` |
| `cargo test/build/clippy/check` | `rtk cargo ...` |
| `cat/head/tail <file>` | `rtk read <file>` |
| `rg/grep <pattern>` | `rtk grep <pattern>` |
| `ast-grep run -p <pattern>` | `rtk ast-grep run -p <pattern>` |
| `ls` | `rtk ls` |
| `tree` | `rtk tree` |
| `wc` | `rtk wc` |
| `jest` | `rtk jest` |
| `vitest` | `rtk vitest` |
| `tsc` | `rtk tsc` |
| `eslint/biome` | `rtk lint` |
| `prettier` | `rtk prettier` |
| `playwright` | `rtk playwright` |
| `prisma` | `rtk prisma` |
| `ruff check/format` | `rtk ruff ...` |
| `pytest` | `rtk pytest` |
| `mypy` | `rtk mypy` |
| `pip list/install` | `rtk pip ...` |
| `go test/build/vet` | `rtk go ...` |
| `golangci-lint` | `rtk golangci-lint` |
| `docker ps/images/logs` | `rtk docker ...` |
| `kubectl get/logs` | `rtk kubectl ...` |
| `curl` | `rtk curl` |
| `pnpm list/outdated` | `rtk pnpm ...` |

### Excluding commands

To prevent some commands from being rewritten, add them to `config.toml`:

```toml
[hooks]
exclude_commands = ["curl", "playwright"]
```

The pattern is anchored at the start of the command: `"curl"` excludes `curl https://...` but not `curl-config`.
Before comparing, RTK strips the wrapper, interpreter or path, so `"playwright"` covers
`playwright test` as well as `npx playwright test` or `pnpm exec playwright test`, and `"pytest"` also covers
`python3 -m pytest tests/`. Arguments are kept: `"^ls$"` excludes bare `ls` without
also matching `ls -la`.

---

## Configuration

### Configuration file

**Location:** `~/.config/rtk/config.toml` (Linux) or `~/Library/Application Support/rtk/config.toml` (macOS)

**Commands:**
```bash
rtk config                # Show the current configuration
rtk config --create       # Create the file with default values
```

### Full structure

```toml
[tracking]
enabled = true              # Enable/disable tracking
history_days = 90           # Retention in days (automatic cleanup)
database_path = "/custom/path/tracking.db"  # Custom path (optional)

[display]
colors = true               # Colored output
emoji = true                # Use emojis
max_width = 120             # Maximum output width

[filters]
ignore_dirs = [".git", "node_modules", "target", "__pycache__", ".venv", "vendor"]
ignore_files = ["*.lock", "*.min.js", "*.min.css"]

[retriever]
mode = "sqlite"             # sqlite (default) | tee (legacy files) | disabled
max_entries = 200           # Max entries in the recall database
retention_days = 30         # Entry retention
# database_path = "/custom/recall.db"  # Custom path (optional)
# A legacy [tee] section is still recognized: mapped to mode = "tee",
# or "disabled" if enabled = false

[telemetry]
enabled = false             # Anonymous telemetry (1 ping/day, requires consent)
# consent_given = true      # Set automatically by `rtk init` or `rtk telemetry enable`
# consent_date = "..."      # Consent date (RFC 3339)

[hooks]
exclude_commands = []       # Commands excluded from automatic rewriting
```

### Environment variables

| Variable | Description |
|----------|-------------|
| `RTK_RECALL=0` | Disable recall saving |
| `RTK_TEE_DIR` | Override the tee directory ("tee" mode) |
| `RTK_TELEMETRY_DISABLED=1` | Disable telemetry |
| `RTK_HOOK_AUDIT=1` | Enable hook auditing |
| `SKIP_ENV_VALIDATION=1` | Disable env validation (Next.js, etc.) |

---

## Recall system

### Raw output recovery

When a command fails (or a filter truncates a list), RTK saves the complete raw output in a local SQLite database, addressed by content hash. This lets the LLM retrieve the output without re-running the command.

**How it works:**
1. The command fails (exit code != 0) or the output is truncated
2. RTK stores the raw output (gzip) in `~/.local/share/rtk/recall.db`
3. A hint with the hash is shown: `[full output: rtk recall <hash>]` or `[+N hidden: rtk recall <hash>]`
4. `rtk recall <hash>` returns the missing part (delta); `--full` returns the complete output

**Commands:**
```bash
rtk recall <hash>            # Part not shown (delta)
rtk recall <hash> --full     # Complete output
rtk recall <hash> --grep RE  # Filter by regex
rtk recall --list            # List entries
rtk gain --recalls           # Retrieval rate per filter
rtk config recall <mode>     # Change mode without editing the config (sqlite|tee|disabled)
```

`rtk gain --recalls` keeps data strictly separated by mode: an exact rate in sqlite mode
(reads go through `rtk recall`), an approximation (`≥`) in tee mode (only bash reads
are detectable through the hook, not the Read tool). A high rate indicates a filter
that hides output the agent comes back for.

**Configuration:**

| Setting | Default | Description |
|---------|---------|-------------|
| `retriever.mode` | `"sqlite"` | `"sqlite"`, `"tee"` (legacy files), `"disabled"` |
| `retriever.max_entries` | `200` | FIFO eviction beyond this |
| `retriever.retention_days` | `30` | Old entries are purged |
| `retriever.max_entry_bytes` | `10 MB` | Truncated beyond this (at the last complete line) |
| Minimum size | 500 bytes | Failures that are too short are not saved |

The `"tee"` mode keeps the legacy behavior (`.log` files in `~/.local/share/rtk/tee/`, rotation via `tee_max_files`). A legacy `[tee]` section in the config is mapped automatically (see above).

Change the mode with `rtk config recall <sqlite|tee|disabled>` (a targeted edit of
config.toml that preserves comments and migrates legacy `[tee]` fields); the equivalent
field for manual editing is `[retriever] mode`.

---

## Telemetry

RTK can send one anonymous ping per day (23-hour interval) for usage statistics. Telemetry is opt-in and **disabled by default**; it requires explicit consent (GDPR Art. 6, 7). `rtk init` asks for consent with a `[y/N]` prompt.

**Data sent:** device hash (SHA-256 of a random salt), version, OS, architecture, command count over 24h, top commands, bash output reduction percentage, recall counters per filter family (elisions/retrievals -- names taken from a closed allowlist, never a hash, path, argument or content).

**Data controller:** `RTK AI Labs`, contact@rtk-ai.app

**Managing telemetry:**
```bash
rtk telemetry status     # Show consent status
rtk telemetry enable     # Give consent (interactive prompt)
rtk telemetry disable    # Withdraw consent
rtk telemetry forget     # Withdraw + delete local data + request server-side erasure
```

**Disable via environment variable:**
```bash
export RTK_TELEMETRY_DISABLED=1
```

No personal data, no command content and no file path is transmitted. Server retention: 12 months max. Details: [docs/TELEMETRY.md](../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](../guide/resources/savings-explained.md) for the full breakdown.

## Where the data lives

Tracking data is stored locally in a SQLite database named `history.db`:

| Platform | Default path |
|----------|--------------|
| Linux | `~/.local/share/rtk/history.db` |
| macOS | `~/Library/Application Support/rtk/history.db` |
| Windows | `%LOCALAPPDATA%\rtk\history.db` |

To use a different location, either set the `RTK_DB_PATH` environment variable (highest priority) or set it in `config.toml`:

```toml
[tracking]
database_path = "/custom/path/history.db"
```

## What is stored

For each command RTK runs, the database records:

| Field | Description |
|-------|-------------|
| `timestamp` | When the command ran (UTC) |
| `original_cmd` | The command as typed (e.g. `git log --oneline -5`) |
| `rtk_cmd` | The RTK command that ran (e.g. `rtk git log --oneline -5`) |
| `project_path` | The working directory, used by `rtk gain --project` |
| `input_tokens` | Estimated tokens in the raw output (`bytes / 4`) |
| `output_tokens` | Estimated tokens after filtering (`bytes / 4`) |
| `saved_tokens` | `input_tokens - output_tokens` |
| `savings_pct` | `saved / input × 100`, a bash output byte ratio |
| `exec_time_ms` | Execution time in milliseconds |

The same database also holds filter parse failures and hook rewrite decisions, which feed `rtk gain` and `rtk session`.

Commands RTK passes through without filtering are tracked at 0% reduction. They still count toward
`commands` and the token totals, but they are left out of `total_time_ms` and `avg_time_ms` (in the
summary, the daily/weekly/monthly breakdowns, and the JSON/CSV exports): only commands with captured
input (`input_tokens > 0`) are timed. A passthrough row filtered nothing, and its duration is the
wrapped process's lifetime — for an interactive TUI such as `rtk claude`, the whole session.

## Privacy

- The tracking database is **local only** and never leaves your machine.
- It is separate from telemetry. Telemetry is off by default and sends only anonymous aggregates if you opt in — see [Telemetry & Privacy](../guide/resources/telemetry.md).

## Retention

Records older than **90 days** are deleted automatically when new records are written.

## Viewing your data

```bash
rtk gain                  # summary
rtk gain --project        # current project only
rtk gain --daily          # day by day
rtk gain --weekly         # week by week
rtk gain --monthly        # month by month
rtk gain --history        # last 10 commands
```

See [Token Savings Analytics](../guide/analytics/gain.md) for the full `rtk gain` surface.

## Exporting

```bash
rtk gain --all --format json > savings.json
rtk gain --all --format csv  > savings.csv
```

Example JSON entry for one day (illustrative values):

```json
{
  "date": "2026-01-01",
  "commands": 100,
  "input_tokens": 500000,
  "output_tokens": 100000,
  "saved_tokens": 400000,
  "savings_pct": 80.0,
  "total_time_ms": 20000,
  "avg_time_ms": 200
}
```

Weekly entries carry `week_start` and `week_end` instead of `date`; monthly entries carry `month` (`YYYY-MM`).

You can also query the database directly:

```bash
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"
```

## Backup

```bash
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db
```

## Reset

```bash
rtk gain --reset          # asks for confirmation
rtk gain --reset --yes    # skip the prompt (scripts)
```

Alternatively, delete the database file; it is recreated on the next command:

```bash
rm ~/.local/share/rtk/history.db
```

## Troubleshooting

### Database locked error

If you see "database is locked" errors:
- Make sure no other tool holds the database open for writing
- Check file permissions on `history.db`
- As a last resort, delete it: `rm ~/.local/share/rtk/history.db && rtk gain`

### Token counts don't match my provider

Token estimation uses `~4 bytes = 1 token`. The percentage is reliable because the same estimator is applied to raw and filtered output, but absolute counts are approximate. For exact counts, run the raw and filtered output through your model's own tokenizer.

## See Also

- [Token Savings Analytics](../guide/analytics/gain.md)
- [Audit Guide](AUDIT_GUIDE.md)
- [README.md](../../README.md)

---

### Guide Analytics Discover (`docs/guide/analytics/discover.md`)

---
title: Discover and Session
description: Find missed savings opportunities with rtk discover, and track RTK adoption with rtk session
sidebar:
  order: 2
---

# Discover and Session

## rtk discover — find missed savings

`rtk discover` analyzes your Claude Code command history to identify commands that ran without RTK filtering, and estimates how much bash output RTK would have removed from them.

```bash
rtk discover                    # analyze current project history
rtk discover --all              # all projects
rtk discover --all --since 7    # last 7 days, all projects
```

**Example output** (illustrative values):

```
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](../resources/savings-explained.md).

If commands appear in the missed list after installing RTK, it usually means the hook isn't active for that agent. See [Troubleshooting](../resources/troubleshooting.md#ai-assistant-not-using-rtk) — "AI assistant not using RTK".

## rtk session — adoption tracking

`rtk session` shows RTK adoption across recent Claude Code sessions: how many shell commands ran through RTK vs. raw.

```bash
rtk session
```

**Example output** (illustrative values):

```
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
description: Measure and analyze the bash output reduction RTK achieves with rtk gain
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](../resources/savings-explained.md) for the full picture.

## Quick reference

```bash
# Default summary
rtk gain

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

# Classic flags
rtk gain --graph          # ASCII graph, last 30 days
rtk gain --history        # last 10 commands
rtk gain --quota          # monthly quota savings estimate (default tier: 20x)
rtk gain --quota -t pro   # use pro tier token budget for estimate
rtk gain --recalls        # recall efficiency per filter (calibration)

# Live dashboard
rtk gain                  # auto-launches the dashboard when the RTK Proxy runs
rtk gain --watch          # force the dashboard
rtk gain --text           # force the static view

# Attribution (where the savings came from)
rtk gain --by-method      # per compression method
rtk gain --by-rule        # per pipeline rule
rtk gain --by-layer       # per token layer (read, tool_result, history, ...)
rtk gain --by-pipeline    # per pipeline
rtk gain --pipeline NAME  # one pipeline, sliced by where its savings land
rtk gain --shadow         # what shadow rules would have saved

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

# Reset
rtk gain --reset          # zero the savings and recall stats (asks first)
```

## Daily breakdown

```bash
rtk gain --daily
```

**Example output** (illustrative values — yours depend entirely on which commands you run):

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

- **Cmds**: RTK commands executed
- **Input**: Estimated tokens from raw command output (`bytes / 4`)
- **Output**: Estimated tokens after filtering (`bytes / 4`)
- **Saved**: Input - Output, in estimated tokens
- **Save%**: Saved / Input × 100 — a **bash output byte ratio**, not a share of your bill

## Weekly and monthly breakdowns

```bash
rtk gain --weekly
rtk gain --monthly
```

Same columns as daily, aggregated by Sunday-Saturday week or calendar month.

## Export formats

| Format | Flag | Use case |
|--------|------|----------|
| `text` | default | Terminal display |
| `json` | `--format json` | Programmatic analysis, dashboards |
| `csv` | `--format csv` | Excel, Python/R, Google Sheets |

**JSON structure:**
```json
{
  "summary": {
    "total_commands": 300,
    "total_input": 1500000,
    "total_output": 300000,
    "total_saved": 1200000,
    "avg_savings_pct": 80.0
  },
  "daily": [...],
  "weekly": [...],
  "monthly": [...]
}
```

## How token estimation works

`rtk gain` estimates tokens as `bytes / 4`; RTK ships no tokenizer. The same estimator is applied to raw and filtered output, so the percentage is reliable; the absolute token counts are approximate and will not match your provider's billing.

```
Input Tokens  = estimate_tokens(raw_command_output)
Output Tokens = estimate_tokens(rtk_filtered_output)
Saved Tokens  = Input - Output
Savings %     = (Saved / Input) × 100
```

## Database

Savings data is stored locally in SQLite:

- **Location**: `~/.local/share/rtk/history.db` on Linux, `~/Library/Application Support/rtk/history.db` on macOS (override with `RTK_DB_PATH`) — paths below use the Linux location
- **Retention**: 90 days (automatic cleanup)
- **Scope**: Global across all projects and Claude sessions

```bash
# Inspect raw data
sqlite3 ~/.local/share/rtk/history.db \
  "SELECT timestamp, rtk_cmd, saved_tokens FROM commands
   ORDER BY timestamp DESC LIMIT 10"

# Backup
cp ~/.local/share/rtk/history.db ~/backups/rtk-history-$(date +%Y%m%d).db

# Reset
rm ~/.local/share/rtk/history.db    # recreated on next command
```

Prefer `--reset` over deleting the file: it zeroes both the token savings and the
[recall stats](#recall-efficiency), which live separately, and leaves the database intact.

```bash
rtk gain --reset          # asks for confirmation
rtk gain --reset --yes    # skip the prompt (scripts)
```

## Analysis workflows

```bash
# Weekly progress: generate a CSV report every Monday
rtk gain --weekly --format csv > reports/week-$(date +%Y-%W).csv

# Monthly budget review
rtk gain --monthly --format json | jq '.monthly[] |
  {month, saved_tokens, quota_pct: (.saved_tokens / 6000000 * 100)}'

# Cron: daily JSON snapshot for a dashboard
0 0 * * * rtk gain --all --format json > /var/www/dashboard/rtk-stats.json
```

**Python/pandas:**
```python
import pandas as pd
import subprocess

result = subprocess.run(['rtk', 'gain', '--all', '--format', 'csv'],
                       capture_output=True, text=True)
lines = result.stdout.split('\n')
daily_start = lines.index('# Daily Data') + 2
daily_end = lines.index('', daily_start)
daily_df = pd.read_csv(pd.StringIO('\n'.join(lines[daily_start:daily_end])))
daily_df['date'] = pd.to_datetime(daily_df['date'])
daily_df.plot(x='date', y='savings_pct', kind='line')
```

**GitHub Actions (weekly stats):**
```yaml
on:
  schedule:
    - cron: '0 0 * * 1'
jobs:
  stats:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
      - run: rtk gain --weekly --format json > stats/week-$(date +%Y-%W).json
      - run: git add stats/ && git commit -m "Weekly rtk stats" && git push
```

## Quota estimate

`--quota` expresses the estimated tokens saved as a fraction of a monthly subscription budget. Like every other figure in `rtk gain`, it is derived from the `bytes / 4` estimate of bash output, so treat it as an order of magnitude rather than a billing forecast.

```bash
rtk gain --quota          # uses 20x tier by default
rtk gain --quota -t pro   # Claude Pro plan budget
rtk gain --quota -t 5x    # 5× usage plan budget
rtk gain --quota -t 20x   # 20× usage plan budget
```

The tiers (`pro`, `5x`, `20x`) correspond to Anthropic Claude API subscription levels, each with a different monthly token allocation. RTK uses those allocations as a denominator to express your savings as a percentage of your budget.

:::tip[Find missed savings]
`rtk gain` shows what RTK saved. To find commands that ran *without* RTK and calculate what you lost, see [rtk discover](./discover.md).
:::

## 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](../proxy/overview.md) compressing billed input tokens.

```bash
rtk gain --watch
```

### Auto-launch

A bare `rtk gain` launches the dashboard **automatically** once the RTK Proxy is running. All four conditions must hold:

1. You passed no flag asking for a specific static view — no `--daily`, `--weekly`, `--monthly`, `--all`, `--history`, `--graph`, `--quota`, and `--format` left at `text`.
2. You did not pass `--text`.
3. Standard output is an interactive terminal — piping or redirecting opts out, so scripts and cron jobs are unaffected.
4. The RTK Proxy is running and reachable.

Without the proxy running, `rtk gain` behaves exactly as it always has.

`--text` is the escape hatch — it forces the static view with the proxy up:

```bash
rtk gain --text
```

`--watch` and `--text` are mutually exclusive. Asking for `--watch` without a terminal falls back to the text view with a warning rather than failing.

## Attribution

The default report tells you *how much* was saved. The attribution flags tell you *where it came from*, which helps you tune a pipeline. Each covers both the hook and proxy sites; proxy figures appear only if you run the [RTK Proxy](../proxy/overview.md).

```bash
rtk gain --by-method      # which compression method did the work
rtk gain --by-rule        # which pipeline rule fired
rtk gain --by-layer       # which token layer (read, tool_result, history, ...)
rtk gain --by-pipeline    # which pipeline
```

These combine — passing several renders several sections in one report.

### One pipeline in detail

`--pipeline <NAME>` slices a single pipeline by where its savings actually land: which tool outputs the hooks trimmed, and which methods compressed billed input.

```bash
rtk gain --pipeline my-pipeline
```

### Shadow rules

Shadow rules are recorded but never emitted — they let you measure a rule's effect before trusting it with real output. `--shadow` reports what they *would* have saved:

```bash
rtk gain --shadow
```

A shadow rule showing strong savings is a candidate to promote; one showing nothing is a rule you can delete.

:::note[Full edition only]
Rule, pipeline and shadow attribution describe the compression engine, which the Apache-2.0 edition does not include. See [Editions and Licensing](../resources/editions.md).
:::

## Recall efficiency

`--recalls` measures how often your AI assistant goes back for output a filter elided — the signal that a filter's cap is too aggressive for your workflow. Every time a filter stores elided output (an *elision*) and every time the assistant retrieves it (a *recall*), RTK counts it per filter:

```bash
rtk gain --recalls
```

Illustrative output:

```
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](../getting-started/configuration.md#recall-system), reads are shell commands (`tail`, `cat`, `grep`, …) observed by the rewrite hook before execution. Editor or assistant file-tool reads are invisible, denied commands are not counted, and the per-file dedup window is finite — so the `≥` rate is approximate, not exact. A high rate is still a reliable signal that the cap is too aggressive.

Stats survive entry eviction and retention cleanup: calibration data is kept even after the underlying outputs are purged.

## Troubleshooting

**No data showing:**
```bash
ls -lh ~/.local/share/rtk/history.db
sqlite3 ~/.local/share/rtk/history.db "SELECT COUNT(*) FROM commands"
git status    # run any tracked command to generate data
```

**Incorrect statistics:** Token estimation is a heuristic. For precise counts, use `tiktoken`:
```bash
pip install tiktoken
git status > output.txt
python -c "
import tiktoken
enc = tiktoken.get_encoding('cl100k_base')
print(len(enc.encode(open('output.txt').read())), 'actual tokens')
"
```

---

### Guide Getting Started Configuration (`docs/guide/getting-started/configuration.md`)

---
title: Configuration
description: Customize RTK behavior via config.toml, environment variables, and per-project filters
sidebar:
  order: 4
---

# Configuration

## Config file location

| Platform | Path |
|----------|------|
| Linux | `~/.config/rtk/config.toml` |
| macOS | `~/Library/Application Support/rtk/config.toml` |

```bash
rtk config            # show current configuration
rtk config --create   # create config file with defaults
```

## Full config structure

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

For full details on what is collected, opt-out options, and GDPR rights, see [Telemetry & Privacy](../resources/telemetry.md).

## Awareness level

`rtk init` writes a short instructions file for your agent (`~/.claude/RTK.md`, `~/.gemini/GEMINI.md`, …).
`awareness.level` sets how much it says about RTK. Output is condensed the same way at every level.

| Level | The agent is told | Pick it when |
|-------|-------------------|--------------|
| `default` | How to read condensed output. Nothing about RTK. | Hook-based agent, RTK stays invisible. |
| `high` | `default` + what RTK is and `rtk gain`, `rtk bypass`, `RTK_DISABLED=1`, `rtk discover`. | You want to ask the agent about savings or to bypass RTK. |
| `full` | `high` + "prefix every command with `rtk`". | Agent without a hook, or you want the agent to drive RTK itself. |

```toml
[awareness]
level = "high"
```

Re-run `rtk init -g` (or your agent's init command) to rewrite the file.

Agents without a hook (Codex CLI, Cline, Windsurf, Kilo Code, Kimi) always get `full`,
since the agent must type `rtk` itself. `rtk init` prints a note when it does this.

## Environment variables

| Variable | Description |
|----------|-------------|
| `RTK_DISABLED=1` | Disable RTK for a single command (`RTK_DISABLED=1 git status`) |
| `RTK_RECALL=0` | Disable the recall store for a single command |
| `RTK_RECALL_DB` | Override the recall database path |
| `RTK_TEE=0` | Legacy alias of `RTK_RECALL=0` (still honored) |
| `RTK_TEE_DIR` | Override the tee directory (tee mode) |
| `RTK_TELEMETRY_DISABLED=1` | Disable telemetry |
| `RTK_HOOK_AUDIT=1` | Enable hook audit logging |
| `RTK_SUPPRESS_HOOK_WARNING=1` | Suppress the missing-hook warning only (not the outdated-hook upgrade prompt); `true`, `yes`, `on` also accepted |
| `RTK_SUPPRESS_HOOK_WARNING=0` | Force the missing-hook warning back on, overriding `hooks.suppress_hook_warning`; `false`, `no`, `off` also accepted |
| `SKIP_ENV_VALIDATION=1` | Skip env validation (useful with Next.js) |

## Recall system

When a command fails — or a filter trims a long list — RTK persists the full output to an embedded database and prints a recall hint:

```
FAILED: 2/15 tests
[full output: rtk recall 36365b69eda6c4f1]
```

Your AI assistant runs `rtk recall <hash>` exactly as printed in the hint — that is the whole agent interface. For humans inspecting the store: `rtk recall <hash> --full | --from N | --lines N | --grep PAT` and `rtk recall --list`. Storage is byte-faithful (`BLOB` + lossless gzip); the stored input is the captured command text, as with the previous tee files.

### Choosing the recovery mode

The simplest way is the CLI — no file editing needed:

```bash
rtk config recall           # show the active mode and its source
rtk config recall sqlite    # hash-addressed sqlite store (default)
rtk config recall tee       # legacy .log files in ~/.local/share/rtk/tee/
rtk config recall disabled  # no recovery storage
```

Setting a mode rewrites only the relevant keys in `config.toml` (comments and other sections are preserved), and migrates a legacy `[tee]` section — its `max_files`/`max_file_size`/`directory` values are carried over. The equivalent config field, if you prefer editing the file directly, is `[retriever] mode = "sqlite" | "tee" | "disabled"` (see the full structure above).

To see how often your assistant actually goes back for elided output — and which filter caps deserve tuning — see [`rtk gain --recalls`](../analytics/gain.md#recall-efficiency).

| Setting | Default | Description |
|---------|---------|-------------|
| `retriever.mode` | `"sqlite"` | `sqlite` (default), `tee` (legacy files), `disabled` |
| `retriever.max_entry_bytes` | `10485760` | Per-entry storage cap (10 MiB) |
| `retriever.max_entries` | `200` | FIFO cap on retained entries (0 = no cap) |
| `retriever.retention_days` | `30` | Age eviction in days (0 = off) |
| `retriever.compression` | `true` | gzip stored blobs (lossless) |
| `retriever.tee_max_files` | `20` | tee mode: how many files are kept before rotation |
| `retriever.tee_max_file_size` | `1048576` | tee mode: per-file cap (1 MiB) |
| `retriever.tee_on_success` | `false` | tee mode: also archive successful runs — the legacy `[tee] mode = "always"` |
| Max file size | 1 MB | Truncated above this |

## Excluding commands from auto-rewrite

Prevent specific commands from being rewritten by the hook:

```toml
[hooks]
exclude_commands = ["git rebase", "git cherry-pick", "docker exec"]
```

Patterns match against the full command after stripping env prefixes (`VAR=val`), so `"psql"` excludes both `psql -h localhost` and `PGPASSWORD=x psql -h localhost`.

Subcommand patterns work too: `"git push"` excludes `git push origin main` but not `git status`.

An entry names a tool RTK has a filter for, and covers the wrapper, interpreter and path spellings
of it. Before matching, RTK peels those off the command and matches what is left, so
`"playwright"` excludes `playwright test`, `npx playwright test` and `pnpm exec playwright test`
alike; `"pytest"` also covers `python3 -m pytest tests/`, and `"phpunit"` covers
`vendor/bin/phpunit` and `php vendor/bin/phpunit`.

Three spellings are not peeled, and still rewrite despite a matching entry:

| Entry | Command | Why |
|---|---|---|
| `"head"`, `"tail"` | `head -20 f`, `tail -n 5 f` | The line-range form takes a fast path that returns before the exclusion is consulted. Without a line range, `head f` is excluded normally. |
| `"gradlew"`, `"mvn"` | `gradlew.bat build`, `mvnw.cmd test` | Path stripping splits on `/`, so a `.bat`/`.cmd` spelling never reduces to the tool name. `./gradlew` and `gradlew` are both excluded. |
| `"golangci-lint"` | `golangci run ./...` | `golangci run` is one of the rule's own aliases and is kept whole, so it does not match the `golangci-lint` entry. Exclude `"golangci"` as well to cover it. |

A tool RTK has no filter of its own for is matched as typed, because RTK only sees the wrapper:
with `["my-tool"]`, `npx my-tool` still rewrites to `rtk npx my-tool`. Exclude `"npx"` to stop
that.

The arguments are kept when peeling, so an anchored pattern still narrows the way you wrote it:
`"^ls$"` excludes a bare `ls` without swallowing `ls -la`. Matching stays exact — `"go"` never
excludes `golangci-lint`, subcommand patterns stay literal (`"git push"` does not widen to all of
`git`), and an entry never leaks to a different tool that happens to share an RTK filter: `"read"`
does not exclude `cat`, and `"eslint"` does not exclude `biome`.

Patterns starting with `^` are treated as regex:

```toml
[hooks]
exclude_commands = ["^curl", "^wget", "git rebase"]
```

Invalid regex patterns fall back to prefix matching.

Or for a single invocation:

```bash
RTK_DISABLED=1 git rebase main
```

## Telemetry

Telemetry is **off by default** and only runs after you opt in — `rtk init` asks once (`[y/N]`, default no), or run `rtk telemetry enable`. Once enabled, RTK sends at most one anonymous ping per day. No personal data, no file paths, no command content.

Data sent: device hash, version, OS, architecture, command count/24h, top commands, savings %.

To opt out again:

```bash
rtk telemetry disable              # withdraw consent
export RTK_TELEMETRY_DISABLED=1    # block telemetry regardless of config
```

## Custom filters

Add your own filters (or override built-ins) in either location:

- **Project-local** — `.rtk/filters.toml` in your project root (committed with the repo)
- **User-global** — `~/.config/rtk/filters.toml` (applies to every project)

See [`src/filters/README.md`](https://github.com/rtk-ai/rtk/blob/master/src/filters/README.md) for the full TOML DSL reference.

### Trusting custom filters

Because a filter can rewrite what your AI assistant sees, custom filter files are **not applied until you trust them**. An untrusted (or edited) filter file is skipped silently on the command path. You review and manage trust with explicit commands:

```bash
rtk trust      # shows each filter and asks to confirm (--yes to skip the prompt)
rtk untrust    # revokes trust
```

`rtk init` also detects existing filters and lets you enable them — interactively, or non-interactively with `--trust-filters` / `--no-trust-filters`. Trust is tied to the file's contents (SHA-256), so editing a trusted file requires re-running `rtk trust`.

> **Upgrading:** earlier versions applied `~/.config/rtk/filters.toml` without trust. After upgrading, the user-global file is gated like project filters — if you already relied on a global filter, run `rtk trust` once to re-enable it.

---

### Guide Getting Started Installation (`docs/guide/getting-started/installation.md`)

---
title: Installation
description: Install RTK via curl, Homebrew, winget, Cargo, or from source, and verify the correct version
sidebar:
  order: 1
---

# Installation

## Name collision warning

Two unrelated projects share the name `rtk`. Make sure you install the right one:

- **Rust Token Killer** (`rtk-ai/rtk`) — this project, a token-saving CLI proxy
- **Rust Type Kit** (`reachingforthejack/rtk`) — a different tool for generating Rust types

The easiest way to verify you have the correct one: run `rtk gain`. It should display the savings dashboard. If it returns "command not found", you either have the wrong package or RTK is not installed.

## Check before installing

```bash
rtk --version   # should print: rtk x.y.z
rtk gain        # should show the savings dashboard
```

If both commands work, RTK is already installed. Skip to [Project initialization](#project-initialization).

## Quick install (Linux and macOS)

```bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
```

Installs to `~/.local/bin`. Add it to your PATH if it is not there already:

```bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc   # or ~/.zshrc
```

## Homebrew (macOS and Linux)

```bash
brew install rtk-ai/tap/rtk
```

## winget (Windows)

```powershell
winget install rtk-ai.rtk
```

## Cargo

:::caution[Name collision risk]
`cargo install rtk` may install **Rust Type Kit** instead of Rust Token Killer — two unrelated projects share the same crate name. Use the explicit Git URL to guarantee the correct package:
:::

```bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
```

## Pre-built binaries (Windows, Linux, macOS)

Download from [GitHub releases](https://github.com/rtk-ai/rtk/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](#windows) below — the hook system works natively, no WSL required.

## Apache-2.0 edition

The same `rtk` binary built without the BUSL-1.1 compression engine — the hook rewrite system and every command filter, under Apache-2.0 alone. Pick this if your policy does not allow a source-available licence in the build.

```bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | RTK_EDITION=oss sh
brew install rtk-ai/tap/rtk-oss
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk --no-default-features
```

Release assets are named `rtk-oss-<target>`, with `rtk-oss` Debian and RPM packages alongside. The binary is still called `rtk`, so hooks and scripts are unchanged — but the two editions conflict and cannot be installed together.

See [Editions and Licensing](../resources/editions.md) 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](#winget-windows) if you can — it handles PATH for you.

```powershell
# 1. Download and extract rtk-x86_64-pc-windows-msvc.zip from releases
# 2. Add rtk.exe to your PATH (e.g. C:\Users\<you>\.local\bin)
# 3. Initialize - installs the native binary hook
rtk init -g
```

**Upgrading from an older install?** If you set RTK up before v0.37.2 you may still have the legacy `rtk-rewrite.sh` shell hook, which does need a Unix shell. Re-run `rtk init -g` to migrate to the native binary hook.

**Prerequisites**: some filters shell out to [ripgrep](https://github.com/BurntSushi/ripgrep) (`rg`). Install it and keep it on your PATH to avoid `Binary 'rg' not found on PATH` warnings:

```powershell
winget install BurntSushi.ripgrep.MSVC
```

### WSL

[WSL](https://learn.microsoft.com/en-us/windows/wsl/install) also works and behaves exactly like Linux:

```bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
rtk init -g
```

| Feature | Native Windows | WSL |
|---|---|---|
| Filters (cargo, git, …) | Full | Full |
| Auto-rewrite hook | Yes (native binary) | Yes |
| `rtk init -g` | Hook mode | Hook mode |
| `rtk gain` / analytics | Full | Full |

## Verify installation

```bash
rtk --version   # rtk x.y.z
rtk gain        # savings dashboard
```

If `rtk gain` fails but `rtk --version` succeeds, you installed Rust Type Kit by mistake. Uninstall it first:

```bash
cargo uninstall rtk
```

Then reinstall using one of the methods above.

## Project initialization

For a single project, `rtk init` adds the RTK block to `./CLAUDE.md` and a `.rtk/filters.toml` template, without installing a hook:

```bash
rtk init
```

To install the Claude Code hook (registered in `~/.claude/settings.json`, applies to every project):

```bash
rtk init --global
```

## Uninstall

```bash
rtk init -g --uninstall    # remove hook, RTK.md, and settings.json entry
cargo uninstall rtk         # remove binary (if installed via Cargo)
brew uninstall rtk          # remove binary (if installed via Homebrew)
winget uninstall rtk-ai.rtk # remove binary (if installed via winget)
```

---

### Guide Getting Started Quick Start (`docs/guide/getting-started/quick-start.md`)

---
title: Quick Start
description: Get RTK running in 5 minutes and see your first token savings
sidebar:
  order: 2
---

# Quick Start

This guide walks you through your first RTK commands after installation.

## Prerequisites

RTK is installed and verified:

```bash
rtk --version   # rtk x.y.z
rtk gain        # shows token savings dashboard
```

If not, see [Installation](./installation.md).

## Step 1: Initialize for your AI assistant

```bash
# For Claude Code (global — applies to all projects)
rtk init --global

# For a single project only (CLAUDE.md instructions, no hook)
cd /your/project && rtk init
```

`rtk init --global` installs the hook that automatically rewrites commands. Restart your AI assistant after this step.

Using something other than Claude Code? Every agent has its own flag:

```bash
rtk init -g --gemini            # Gemini CLI
rtk init -g --codex             # Codex (OpenAI)
rtk init -g --agent cursor      # Cursor
rtk init -g --agent windsurf    # Windsurf
rtk init --agent cline          # Cline / Roo Code
```

See [Supported agents](./supported-agents.md) for the full list, including per-agent uninstall and the project-local vs global distinction.

### Preview without writing: `--dry-run`

To see exactly what `init` would change before it touches anything, add `--dry-run`:

```bash
rtk init --global --dry-run
```

Every would-be file create/update/patch is printed with a `[dry-run] would ...` prefix, then a `[dry-run] Nothing written.` footer. Nothing on disk is modified, no settings.json is patched, and the telemetry consent prompt is skipped. Combine with `-v` to also print the full content RTK would write:

```bash
rtk init --global --dry-run -v
```

`--dry-run` works for every init flavour (`--agent cursor`, `--gemini`, `--codex`, `--copilot`, `--uninstall`, ...). It cannot be combined with `--show`.

## Step 2: Use your tools normally

Once the hook is installed, nothing changes in how you work. Your AI assistant runs commands as usual — the hook intercepts them transparently and rewrites them before execution.

For example, when Claude Code runs `cargo test`, the hook rewrites it to `rtk cargo test` before it executes. The LLM receives filtered output with only the failures — not 500 lines of passing tests. You never see or type `rtk`.

RTK covers all major ecosystems — Git, Cargo/Rust, JavaScript, Python, Go, Ruby, .NET, Docker/Kubernetes, and more. See [What RTK Optimizes](../resources/what-rtk-covers.md) for the full list.

## Step 3: Check your savings

After a few commands, see how much was saved:

```bash
rtk gain
```

Illustrative 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](../pipelines/overview.md).
:::

Simple quoted shell wrappers are rewritten too, without changing the shell they name:

```bash
bash -c "head foo && grep -R bar ."
# → bash -c "rtk read foo --head-lines 10 && rtk grep -R bar ."
```

This covers exact `sh -c`, `bash -c`, `zsh -c` and `fish -c` wrappers around a quoted script. A `fish -c` script is read as fish, so fish's own syntax (`(cmd)` substitution, `and`/`or`/`end`) is left alone rather than rewritten under POSIX rules:

```bash
fish -c 'git status; cargo test'      # → fish -c 'rtk git status; rtk cargo test'
fish -c 'git status; and cargo test'  # unchanged
```

Shell expansion inside an outer double quote, extra shell options, redirects to files and nested wrappers pass through unchanged.

## Step 4: Unsupported commands

Commands RTK doesn't recognize run through passthrough — output is unchanged, usage is tracked:

```bash
rtk bypass make install
```

## Next steps

- [What RTK Optimizes](../resources/what-rtk-covers.md) — all supported commands and savings by ecosystem
- [Supported agents](./supported-agents.md) — Claude Code, Cursor, Copilot, and more
- [Configuration](./configuration.md) — customize RTK behavior
- [Token Savings Analytics](../analytics/gain.md) — the full `rtk gain` surface
- [Editions and Licensing](../resources/editions.md) — what each download contains

---

### Guide Getting Started Supported Agents (`docs/guide/getting-started/supported-agents.md`)

---
title: Supported Agents
description: How to integrate RTK with Claude Code, Cursor, Copilot, Cline, Windsurf, Codex, OpenCode, Hermes, Kilo Code, Antigravity, Factory Droid, Mistral Vibe, and Trae
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 output
```

## Supported agents

| Agent | Integration tier | Can rewrite transparently? |
|-------|-----------------|---------------------------|
| Claude Code | Rust binary (`PreToolUse`, `rtk hook claude`) | Yes |
| Trae | Rust binary (`PreToolUse`, matcher `RunCommand`) | Yes |
| VS Code Copilot Chat | Rust binary (`PreToolUse`, `rtk hook copilot`) | Yes |
| GitHub Copilot CLI | Rust binary (`PreToolUse`, `rtk hook copilot`) | Yes |
| Cursor | Rust binary (`preToolUse`, `rtk hook cursor`) | Yes |
| Gemini CLI | Rust binary (`BeforeTool`) | Yes |
| OpenCode | TypeScript plugin (`execute.before` in 2.0, `tool.execute.before` in 1.x >= 1.18.29) | Yes |
| OpenClaw | TypeScript plugin (`before_tool_call`) | Yes |
| Pi | TypeScript extension (`tool_call` event) | Yes |
| Oh My Pi (OMP) | TypeScript extension (`tool_call` event, shared with Pi) | Yes |
| Hermes | Python plugin (`terminal` command mutation) | Yes |
| Factory Droid | Rust binary (`PreToolUse`, matcher `Execute`) | Yes |
| Cline / Roo Code | Rules file (prompt-level) | N/A |
| Windsurf | Rules file (prompt-level) | N/A |
| Codex CLI | Rust binary (`PreToolUse`) | Yes |
| Kilo Code | Rules file (prompt-level) | N/A |
| Google Antigravity | Rust binary (`PreToolUse` plugin) | Yes |
| Mistral Vibe | Rust binary (`pre_tool`) | Yes |

Agents that rewrite transparently receive the awareness file selected by `awareness.level` in
`config.toml` (`default` says nothing about RTK). Rules-file agents cannot rewrite, so the agent
must prefix `rtk` itself; they always receive the `full` awareness file and `rtk init` says so.
See [Configuration](configuration.md#awareness-level).

:::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](../resources/agent-coverage.md) for the per-agent table.
:::

## Installation by agent

### Claude Code

```bash
rtk init --global    # installs hook + patches settings.json
```

Restart Claude Code. Verify:

```bash
rtk init --show    # shows hook status
```

### Trae

```bash
rtk init --agent trae             # project-scoped (.trae/hooks.json)
rtk init --global --agent trae    # user-scoped (~/.trae/hooks.json)
```

Global installation also updates `~/.trae-cn/hooks.json` when the `.trae-cn` directory already exists. Both modes install the native `rtk hook trae` command as a `PreToolUse` hook for `RunCommand`.

Configuration files and hook input may include a UTF-8 BOM. Existing `rtk.exe hook trae` registrations are also recognized during install and uninstall.

If a global install fails while writing a target, the error lists any targets already updated. After fixing the reported filesystem error, rerun the install; completed targets will not receive duplicate hooks.

The hook returns `hookSpecificOutput.updatedInput`, preserving fields such as `description` and `timeout` while replacing only `command`. It deliberately omits `permissionDecision`, leaving command approval to Trae. Commands containing command substitution, process substitution, heredocs, or file-target redirects are left unchanged so Trae evaluates the original command natively.

Uninstall:

```bash
rtk init --uninstall --agent trae
rtk init --uninstall --global --agent trae
```

Uninstall removes only RTK-managed hook entries; unrelated Trae hooks and configuration are preserved.

### Cursor

```bash
rtk init --global --agent cursor
```

Restart Cursor. The hook uses `preToolUse` with Cursor's `updated_input` format.

### GitHub Copilot (VS Code Chat + CLI)

```bash
rtk init --copilot            # project-scoped (.github/hooks/)
rtk init --global --copilot   # user-scoped (~/.copilot/hooks/, respects $COPILOT_HOME)
```

Project-scoped writes `.github/hooks/rtk-rewrite.json` — a single `PreToolUse` entry shared by both hosts, each getting transparent rewrite via `updatedInput` — plus the RTK block in `.github/copilot-instructions.md`. User-scoped writes the same hook config to `~/.copilot/hooks/rtk-rewrite.json` and the RTK block to `~/.copilot/copilot-instructions.md` (both respect `$COPILOT_HOME` if set).

Earlier `rtk` versions also registered a second, camelCase `preToolUse` entry, which made Copilot CLI run the hook twice per tool call. Re-run `rtk init --copilot` (or `--global --copilot`) to upgrade an existing install to the single-hook config.

Uninstall:

```bash
rtk init --uninstall --copilot
rtk init --uninstall --global --copilot
```

Removes only RTK's hook file (and, for project, the RTK block in `copilot-instructions.md`). Other files in `.github/hooks/` or `~/.copilot/hooks/` and your own instruction content are untouched.

### Gemini CLI

```bash
rtk init --global --gemini
```

### OpenCode

```bash
rtk init --global --opencode
```

Installs the Claude Code setup and creates `~/.config/opencode/plugins/rtk.ts`. The plugin registers both OpenCode 2.0 (`execute.before`) and OpenCode 1.x (`tool.execute.before`) hooks, in one default export carrying both entrypoints. That shape is supported from OpenCode 1.18.29, the floor OpenCode documents for it; with an older OpenCode, when it can read `opencode --version`, `rtk init` writes nothing and says so.

### Pi

```bash
# Project-local (default)
rtk init --agent pi

# Global — all projects
rtk init --agent pi --global
```

Creates `.pi/extensions/rtk.ts` (local) or `~/.pi/agent/extensions/rtk.ts` (global). Pi auto-discovers extensions from both paths on startup. The global path follows `PI_CODING_AGENT_DIR` when set; OMP uses that same variable. When the paths alias, RTK records the agent(s) it installed in an adjacent hidden `.rtk-agents` state file so shared-file warnings are based on ownership rather than path equality alone.

Installation updates only the current or a known historical RTK extension. If the managed path contains modified or unrelated content, RTK asks before overwriting it; use `--auto-patch` to approve without prompting or `--no-patch` to leave it unchanged. A declined protected update, including `--no-patch`, exits nonzero so automation can detect that no install occurred. `--dry-run` reports the prompt without changing files. LF and CRLF line endings are treated as the same stock extension.

Uninstall:

```bash
rtk init --uninstall --agent pi
rtk init --uninstall --agent pi --global
```

Removes only the current or known historical stock Pi extension. If the file contains modified RTK content, a normal uninstall aborts without deleting it, while `--dry-run` previews that refusal; unreadable content is left in place and causes a normal uninstall to exit nonzero, while `--dry-run` reports it and succeeds. Unrelated content is left in place. LF and CRLF stock files are both recognized.

### Oh My Pi (OMP)

```bash
# Project-local (default)
rtk init --agent omp

# Global — all projects
rtk init --agent omp --global
```

Creates `.omp/extensions/rtk.ts` (local) or `~/.omp/agent/extensions/rtk.ts` (global). OMP loads the same extension file as Pi through its `legacy-pi-compat` layer, so both agents share `rtk.ts`. The global path follows `PI_CODING_AGENT_DIR`, which OMP also honors for its agent directory.

When the Pi and OMP targets in either project or global scope resolve to one file, RTK records whether Pi, OMP, or both agents were installed for that alias in an adjacent hidden `.rtk-agents` state file. A valid sidecar is authoritative. Missing or unreadable state is treated as uncertain: RTK warns and proceeds without using a heuristic to claim sole ownership. Uninstalling a definitively shared project or global file asks before removing it, so confirm that neither agent should use it first (or pass `--auto-patch` to approve). An uncertain legacy or corrupt-state uninstall warns and proceeds; a declined definitive shared uninstall, including `--no-patch`, exits nonzero. `--dry-run` remains a successful preview. RTK currently targets OMP's default profile and `.omp` project directory; named OMP profiles and custom `PI_CONFIG_DIR` locations are not auto-detected.

Installation updates only the current or a known historical RTK extension. If the managed path contains modified or unrelated content, RTK asks before overwriting it; use `--auto-patch` to approve without prompting or `--no-patch` to leave it unchanged. A declined protected update, including `--no-patch`, exits nonzero so automation can detect that no install occurred. `--dry-run` reports the prompt without changing files. LF and CRLF line endings are treated as the same stock extension.

Uninstall:

```bash
rtk init --uninstall --agent omp
rtk init --uninstall --agent omp --global
```

Removes only the current or known historical stock OMP extension. If the file has been modified after install, a normal uninstall aborts with a message instead of removing it, while `--dry-run` previews that refusal; unreadable content is left in place and causes a normal uninstall to exit nonzero, while `--dry-run` reports it and succeeds. Unrelated content is left in place. When Pi and OMP paths in either scope resolve to the same file and a valid sidecar records both agents, uninstall asks before removing the shared file; use `--auto-patch` to approve or `--no-patch` to keep it. Missing or unreadable ownership state warns and proceeds without definitive shared-file protection. A declined definitive shared uninstall exits nonzero so scripts can detect that the file remains.

### OpenClaw

```bash
openclaw plugins install ./openclaw
```

Plugin in the `openclaw/` directory. Uses the `before_tool_call` hook, delegates to `rtk rewrite`.

**Permissions.** RTK keeps the deny gate; OpenClaw owns approval. The plugin runs `rtk rewrite` with `RTK_REWRITE_HOST=openclaw`, which tells RTK that this host applies its own exec policy (`tools.exec.mode`, `security`, `ask`) to whatever the hook returns. RTK therefore does not prompt for a command that matched **no** rule, instead of raising a second approval derived from Claude Code's `settings.json` on a runtime that never opted into it ([#3908](https://github.com/rtk-ai/rtk/issues/3908)).

A command matching a `permissions.deny` rule in those same Claude Code settings files is still refused, and the plugin blocks the tool call — naming the host only relaxes a *default* ask. As in Claude Code, a deny or ask rule matches the command as written and the command behind any leading assignment or wrapper (`HUSKY=0`, `timeout 30`, `nohup`, `command`, ...), each layer in turn, but not every way of invoking the program (`Bash(git push *)` does not stop `git -C . push` or `env -i git push`), so a deny rule is not a security boundary. Allow rules match the command as written. A command matching a `permissions.ask` rule you wrote still prompts when RTK rewrites it, because RTK keeps returning exit 3 for it. Commands containing a command substitution or a redirect to a file are never rewritten, on any host.

The exec tool's own checks see the rewritten command. OpenClaw carries hook adjustments forward into the parameters passed to the exec tool, so `tools.exec.mode`, `tools.exec.security`, `tools.exec.ask` and the exec-approvals allowlist are all matched against `rtk git push`, not `git push`; write those rules against the `rtk` form. That was already true before the permission change. A trusted tool policy (`api.registerTrustedToolPolicy(...)`) is the exception: OpenClaw runs trusted policies before ordinary `before_tool_call` hooks, so one of those still sees the original command.

No minimum rtk version: an rtk that predates `RTK_REWRITE_HOST` ignores it and keeps its previous behaviour, which is a prompt rather than a missing gate — an older rtk prompts for more commands, since it cannot collapse the default ask.

### Hermes

```bash
rtk init --agent hermes
```

Creates `~/.hermes/plugins/rtk-rewrite/` and enables it through `plugins.enabled` in the Hermes config. Hermes loads Python plugins, so the plugin entrypoint is Python, but it is only a thin adapter. It mutates the Hermes `terminal` tool `command` before execution and delegates all rewrite decisions to `rtk rewrite`.

The plugin fails open. If `rtk` is missing at load time, the hook is not registered. If `rtk rewrite` errors, the tool is not `terminal`, the payload has no string `command`, or the plugin raises an exception, Hermes runs the original command unchanged. The same `rtk rewrite` limitations apply: already-prefixed `rtk` commands, compound shell commands, heredocs, and commands without filters are not rewritten.

### Factory Droid

```bash
rtk init -g --agent droid    # user-scoped (~/.factory/hooks.json)
rtk init --agent droid       # project-scoped (.factory/hooks.json, commit to share)
```

Installs a `PreToolUse` hook (matcher `Execute`) into Droid's canonical `hooks.json` — falling back to the `hooks` key of `settings.json` only when that file already carries live `PreToolUse` hooks. Respects `$FACTORY_HOME_OVERRIDE`.

RTK honors Droid's own permission lists, never another agent's settings. Commands matching an explicit `commandDenylist` or `commandBlocklist` entry — read from all four settings scopes (`~/.factory/settings.json`, `~/.factory/settings.local.json`, `.factory/settings.json`, `.factory/settings.local.json`) — are left untouched so Droid's native confirmation or block fires on the original command. Every other command is rewritten via `updatedInput` with **no** permission decision: Droid's native flow (allowlist, autonomy level, other hooks) decides on the rewritten command. To auto-run rewritten read-only commands, add `rtk`-prefixed entries (e.g. `rtk git status`) to your `commandAllowlist`.

Uninstall:

```bash
rtk init --uninstall -g --agent droid
rtk init --uninstall --agent droid
```

Removes only RTK's hook entry; other hooks and settings are untouched.

### Cline / Roo Code

```bash
rtk init --agent cline    # creates .clinerules in current project
```

Cline reads `.clinerules` as custom instructions. RTK adds guidance telling Cline to prefer `rtk <cmd>` over raw commands.

### Windsurf

```bash
rtk init --global --agent windsurf    # creates .windsurfrules in current project
```

### Codex CLI

```bash
rtk init --codex           # project-scoped (.codex/hooks.json + AGENTS.md)
rtk init --global --codex  # user-global ($CODEX_HOME or ~/.codex/)
rtk init --codex --uninstall           # remove project-scoped integration
rtk init --global --codex --uninstall  # remove user-global integration
```

Restart Codex after installation. Project-scoped hooks must be trusted when Codex prompts. The native `rtk hook codex` processor rewrites supported `Bash` commands through `PreToolUse.updatedInput`; Codex then applies its normal approval and sandbox checks to the rewritten command.

Project-scoped install writes `RTK.md` to the project root, a name RTK does not own there, so it marks the files it wrote. An `RTK.md` is RTK's when it carries that marker, when it opens with the heading RTK wrote before the marker existed, or when it is byte-for-byte one of the payloads RTK shipped in between: install replaces it and uninstall removes it. Any other `RTK.md` is yours — install moves it to `RTK.md.bak` (numbered if that name is taken) and says so, and uninstall keeps it and tells you where it is. In global scope `RTK.md` lives in `$CODEX_HOME` and is always RTK's.

Project-scoped install also refuses, without writing anything, when `.codex/hooks.json` or the `hooks.json.bak` it would write beside it resolves outside the project through a symlink, since registering a hook there would run commands from a directory you never named. Uninstall leaves such a hook registered rather than reaching outside for it, and says so. Use the global scope, with `$CODEX_HOME` set if you want a different directory, to configure Codex outside the project.

`AGENTS.md` and `RTK.md` are not restricted this way, and git stores symlinks, so a repository you clone can ship either as a link pointing outside the clone.

A symlinked `AGENTS.md` is followed and left in place: install appends its `@RTK.md` line to whatever the link names, wherever that is, and the file it rewrites ends up owned by you and readable only by you. That line is inert text, unlike `.codex/hooks.json`, which registers a hook that runs shell commands.

A symlinked `RTK.md` depends on what it points at. If the target is not one RTK wrote, the link itself is moved to `RTK.md.bak` and a fresh file takes its place, so nothing outside is touched. If the target is one RTK wrote — a shared or global `RTK.md`, say — install follows the link and rewrites that file in full.

If you install RTK into repositories you have not read, check what `AGENTS.md` and `RTK.md` are first.

### Kilo Code

```bash
rtk init --agent kilocode    # creates .kilocode/rules/rtk-rules.md in current project
```

Kilo Code reads `.kilocode/rules/` as custom instructions. RTK adds guidance telling Kilo Code to prefer `rtk <cmd>` over raw commands.

### Google Antigravity (CLI, IDE & 2.0)

```bash
rtk init --agent antigravity          # workspace-scoped (<repo>/.agents/plugins/rtk/)
rtk init -g --agent antigravity       # machine-scoped (~/.gemini/config/plugins/rtk/)
```

Installs an Antigravity plugin bundle: `hooks.json` maps `PreToolUse` on `run_command` to the native `rtk hook antigravity` binary, and `rules/AGENTS.md` carries the awareness file selected by `awareness.level`. Before any command executes, RTK rewrites the tool call arguments in place using `overwrite.CommandLine`.

Antigravity checks permissions after lifecycle hooks rewrite a command. If you enforce command allowlists, ensure permitted commands include `rtk` (e.g. `command(rtk git status)` or `command(rtk *)`).

Uninstall:

```bash
rtk init --agent antigravity --uninstall       # workspace
rtk init -g --agent antigravity --uninstall    # global
```

### Mistral Vibe

```bash
rtk init -g --agent vibe                # user-scoped (~/.vibe/hooks.toml)
rtk init -g --agent vibe --hook-only    # skip the ~/.vibe/prompts/rtk.md prompt file
```

Installs a `pre_tool` hook entry (`match = "bash"`, `command = "rtk hook vibe"`, `strict = false`) into `~/.vibe/hooks.toml`, following the contract at [docs.mistral.ai/vibe/code/cli/hooks](https://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's `system_message` field so the rewrite is visible.

Unlike Droid, Vibe does not expose a denylist / allowlist surface in `hooks.toml` for RTK to honor. RTK therefore rewrites every bash command it knows how to compress and defers to Vibe's own permission prompt on the rewritten command; commands RTK doesn't handle pass through unchanged. `strict = false` ensures a hook crash degrades to a warning rather than blocking the tool call.

Alongside the hook, RTK drops a system prompt at `~/.vibe/prompts/rtk.md` describing the RTK conventions to Vibe as a fallback. Use `--hook-only` to skip it.

Install is global-only (Vibe's hook registry is user-scoped). Re-running the installer is a no-op; the RTK entry is detected by its `name = "rtk-rewrite"` field and never duplicated.

Uninstall:

```bash
rtk init -g --agent vibe --uninstall
```

Strips only RTK's `[[hooks]]` block and the `~/.vibe/prompts/rtk.md` file. Any other user-declared hooks in `hooks.toml` are preserved byte-for-byte. `hooks.toml` is removed only when the RTK entry was the sole content.

## Integration tiers explained

| Tier | Mechanism | How rewrites work |
|------|-----------|------------------|
| **Full hook** | Rust binary command (or a thin wrapper around one), intercepts via agent API | Transparent — agent never sees the raw command |
| **Plugin** | TypeScript, JavaScript, or Python in agent's plugin system | Transparent, in-place mutation when the agent allows it |
| **Rules file** | Prompt-level instructions | Guidance only — agent is told to prefer `rtk <cmd>` |

Rules file integrations (Cline, Windsurf, Kilo Code) rely on the model following instructions. Full hook integrations (Claude Code, Trae, Cursor, Gemini, Codex, Factory Droid, Antigravity) apply rewrites before execution whenever RTK supports and can safely attest the command. Plugin integrations (OpenCode, Pi, Hermes) use in-place mutation via the agent's extension or plugin API.

## Windows support

Since v0.37.2 the hooks for Claude Code, Cursor, Copilot, Codex, Trae, Factory Droid and Mistral Vibe are native binary commands (`rtk hook <agent>`), so auto-rewrite works on native Windows — no Unix shell, bash or jq required. `rtk init -g` installs the native hook on Windows exactly as on Linux and macOS, and adds it to Claude Code's `settings.json` once you answer `y` to the prompt (or pass `--auto-patch`).

The Gemini CLI hook is a small bash wrapper around `rtk hook gemini`, so it needs a bash available; [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) behaves identically to Linux.

Setups created before v0.37.2 used CLAUDE.md injection with no hook, or the legacy `rtk-rewrite.sh` shell hook. Re-run `rtk init -g` to migrate them to the native binary hook.

## Graceful degradation

Hooks never block command execution. If RTK is missing, the hook exits cleanly and the raw command runs unchanged:

- RTK binary not found: warning to stderr, exit 0
- Invalid JSON input: pass through unchanged
- RTK version too old: warning to stderr, exit 0
- Filter logic error: fallback to raw command output

## Override: disable RTK for one command

```bash
RTK_DISABLED=1 git status    # runs raw git status, no rewrite
```

Or exclude commands permanently in `~/.config/rtk/config.toml`:

```toml
[hooks]
exclude_commands = ["git rebase", "git cherry-pick"]
```

---

### Guide Pipelines Commands (`docs/guide/pipelines/commands.md`)

---
title: Pipeline Commands
description: Inspect, validate, explain, benchmark and share compression pipelines with rtk pipeline
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](./overview.md) for the concepts and [Methods](./methods.md) 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](../resources/editions.md).
:::

## Quick reference

```bash
rtk pipeline show              # print the resolved pipeline
rtk pipeline validate          # validate every pipeline file, non-zero on error
rtk pipeline test              # parse, resolve and validate every loaded file
rtk pipeline status            # last recorded pipeline load problem, if any
rtk pipeline explain           # what fires for a given agent/tool, and why
rtk pipeline bench <file>      # before/after tokens over a local file
rtk pipeline share             # paste-ready markdown card of a pipeline
rtk pipeline share publish     # publish it: GitHub repo (recommended) or gist
rtk pipeline add <source>      # adopt a shared pipeline: validate, install, report what it needs
rtk pipeline search [query]    # find shared pipelines on GitHub (topic rtk-pipeline)
rtk pipeline promote <name>    # move a shadow install live (--discard to drop it)
rtk pipeline eval <name>       # shadow-evaluate an installed pipeline on your live sessions
rtk pipeline log tail          # the opt-in decision log
```

## Inspecting what is active

```bash
rtk pipeline show
```

Prints the pipeline that actually resolved for this directory. To print a built-in verbatim instead, name it:

```bash
rtk pipeline show rtk:default
rtk pipeline show rtk:posthook
```

## Validating your own pipelines

`validate` exits non-zero on errors, which makes it usable as a pre-commit or CI check:

```bash
rtk pipeline validate
```

`test` is the broader check — it parses, resolves and validates every pipeline file RTK loads, not just the active one. When RTK silently fell back because a file failed to load, `status` is where the reason is recorded:

```bash
rtk pipeline test
rtk pipeline status
```

Validation never writes anything into the agent's session. If a file fails to load while the agent is working, RTK drops **that file only** — your other pipeline files, their deny rules included, stay active — records the problem silently, and shows it the next time you run `rtk gain`, `rtk pipeline status` or `validate`. Every code is listed in [Errors and Warnings](./errors.md).

## Explaining a decision

`explain` answers "why did this get compressed the way it did?" for a hypothetical input:

```bash
rtk pipeline explain --agent claude-code --tool Bash --command "git log -20"
rtk pipeline explain --tool Read --file src/main.rs
rtk pipeline explain --tool Read --file big.json --ranged
```

| Flag | Meaning |
|---|---|
| `--agent` | Agent to explain for (`claude-code`, `cursor`, `cline`, …) |
| `--tool` | Tool name (`Bash`, `Read`, `Grep`, `WebFetch`, …). Defaults to `Bash`, or `Read` with `--file` |
| `--file` | File path, for the `before_tool.read` conditions (file_lines, extension, …) |
| `--ranged` | With `--file`: simulate a ranged read rather than a whole-file read |
| `--format` | Content format (`term`, `json`, `web`, `matches`, `lockfile`) |
| `--command` | Command line, for the `command_*` conditions |
| `--last` | Explain the most recent real decision from the log instead of a hypothetical |

`--file` reads the same facts the hook reads, so a dry run cannot disagree with the live decision. Pipe output in to supply the after-tool size conditions (`bytes`, `lines`, `tokens`); without it a rule gated on output size cannot match:

```bash
rtk pipeline explain --tool Grep < output.txt
```

It reports the position, the rule that fired and why, the methods applied, tokens before and after, the recovery hint — and, for a rule that cannot run on your agent, which capability is missing.

`--last` is usually the fastest route when something surprising just happened — it needs the decision log enabled.

## Shadow mode

```toml
[[hook.after_tool.shell]]
name   = "gentle-term"
when   = { is_rtk_command = false }
use    = ["ansi", "dedup"]
shadow = true
```

The rule runs, its decision and would-be savings are logged, nothing is emitted. `rtk gain --shadow` shows what it would have saved; turn `shadow` off when the numbers convince you. Transforms only, and hook rules only: a `shadow` proxy rule is skipped (W018). To try a whole shared pipeline the same way, see [shadow installs](#trying-a-pipeline-silently-shadow-installs).

## Measuring a pipeline

`bench` runs the resolved chain over a local file and reports before/after tokens:

```bash
rtk pipeline bench my-output.txt
```

Use it to check whether a pipeline change helps on your own data.

## The decision log

Off by default. When enabled it records **decisions and commands, never content**:

```bash
rtk pipeline log on
rtk pipeline log tail                    # most recent 20 entries
rtk pipeline log tail --limit 100
rtk pipeline log show --rule my-rule     # filter by rule name
rtk pipeline log show --limit 200
rtk pipeline log off
```

Each entry records timestamp, session, position, tool category, the command as the agent issued it, the rule that fired, action or methods, tokens before and after, shadow flag and recovery hint — never a tool's output, a file's contents or a directive's text. It lives at `~/.local/share/rtk/pipeline.log` (JSON lines).

```text
12:04:11  hook.after_tool.shell   cargo test        term-default    term        3,812 → 640   recall 1f3a9c
12:04:15  hook.before_tool.read   src/Service.java  shunt-large-read route → bulk-read
12:04:31  hook.after_tool.shell   rtk bulk-read …   —               skipped: is_rtk_command
```

Enable it while debugging a pipeline, then turn it off — it is the input `rtk pipeline explain --last` reads.

## Recovery reporting

RTK counts how often the agent asks for an original back. Reads of the recall store are recorded per tool and reported by `rtk gain --recalls` and the recovery-reads table in `rtk gain --by-method`.

It is reporting only: RTK never disables a rule on its own.

`rtk gain` also breaks savings down `--by-rule`, `--by-method`, `--by-layer` and `--shadow`; directives appear net of their standing cost and tasks net of worker cost. See [Token Savings Analytics](../analytics/gain.md).

## Sharing a pipeline

`share` renders a pipeline's measured impact and the pipeline file itself as one artifact. On a terminal it draws a clean, screenshot-able card; piped or redirected — or with `--md` — it emits the paste-ready **markdown** card instead (so `rtk pipeline share > card.md` keeps working), and the two renderings never mix on one stream.

```bash
rtk pipeline share              # the active pipeline (terminal card)
rtk pipeline share rtk:default  # a named one
rtk pipeline share --md         # the markdown card, even on a terminal
```

The card has two panels. **What it does** is derived from the resolved rules, never hand-written. **Measured impact** reports percentages per lever — command filtering and agent hooks as the share of a tool's output bytes removed, delegated reads as the absolute context a worker kept out of your window, and prompt-cache as the share of input served from cache. Figures are estimates (≈ bytes/4): ratios are reliable, absolutes approximate. Percentages travel between setups; token counts and costs do not, which is why the card leads with percentages.

### Publishing

`rtk pipeline share publish` is the interactive publisher. It offers two channels and walks you through validate → secret scan → preview of what leaves your machine → one confirmation:

- **GitHub repository** (recommended) — one public repo per pipeline, in **your** account: `pipeline.toml` plus a README card. The repo description carries the one-line install command, the repo gets the `rtk-pipeline` topic so `rtk pipeline search` finds it, and stars on [github.com/topics/rtk-pipeline](https://github.com/topics/rtk-pipeline) are the leaderboard.
- **GitHub repository (private)** — same repo, visible only to accounts you grant access. For a team or your own machines: anyone with access installs it with the same `rtk pipeline add github:you/repo` line (their `gh` login provides the access). No topic, no search listing — and private is one visibility toggle away from public, so the secret scan and the warnings apply unchanged.
- **GitHub gist** — a single public link with no discovery. Also available non-interactively as `rtk pipeline share --gist`.

Other hosts: publishing targets GitHub, but **installing** does not — `rtk pipeline add <url>` accepts a raw file URL from anywhere (GitLab raw links included), so nothing stops a pipeline living wherever your team keeps code.

```bash
rtk pipeline share publish       # interactive: choose repo or gist, confirm, publish
rtk pipeline share --gist        # non-interactive gist; prints: rtk pipeline add <url>
```

Everything goes through your own `gh` CLI — RTK never sees or stores a token. If `gh` is missing or logged out, the publisher prints the exact manual steps instead. Publishing the same pipeline again updates the same repo, so its link and stars survive. If the pipeline was itself installed from someone else's repo, the publisher offers to **fork** that repo instead of creating a fresh one, keeping the attribution chain visible on GitHub.

What leaves your machine: the pipeline file and the aggregate percentages from your share card. No commands, no paths, no history — and every number is labeled **self-reported** on the published card.

:::caution[Publishing is public, permanent, and your responsibility]
A repo or gist is public, permanent, and indexable. **You** are responsible for its contents — RTK does not review or endorse shared pipelines. Your pipeline file is published **verbatim**, including any `[upstream.*]` blocks (`base_url`, `api_key_env`). Before publishing, a pre-flight scan checks for API-key shapes, tokens, private-key blocks, absolute home paths, and email addresses; a finding blocks publication unless you explicitly type `publish anyway` (or pass `--force`). The scan is a safety net, not a guarantee: read the card yourself before sharing it.
:::

## Adopting a shared pipeline

`add` is the other half of `share`: one command validates a shared pipeline, installs it, and tells you what it needs.

```bash
rtk pipeline add github:owner/repo              # a published repo (see `share publish`)
rtk pipeline add github:owner/repo@<ref>        # a specific commit, tag, or branch
rtk pipeline add card.md                        # a saved share card (the TOML is auto-extracted)
rtk pipeline add - < card.md                    # or pasted on stdin
rtk pipeline add https://example.com/p          # or any URL
rtk pipeline add https://gist.github.com/u/ID   # or a gist link from `share --gist`
rtk pipeline add card.md --name flash           # name it (default: the pipeline's own name, or the file stem)
rtk pipeline add github:owner/repo --shadow     # try it silently instead of installing (see below)
```

A `github:` source (a plain `https://github.com/owner/repo` link works too) fetches the repo's `pipeline.toml` **pinned to a resolved commit** — the commit is recorded locally, so nothing the author pushes later changes what you installed. A `gist.github.com` page link is resolved to its raw content automatically.

It validates in memory first — a pipeline that does not parse or type-check installs **nothing**. On success it writes a named pipeline to `~/.config/rtk/pipelines/<name>.toml` (merged with your others, active immediately), then prints any missing auth as the exact `export` to run, and a `rtk llm-proxy start` line only if the pipeline actually has proxy rules.

On a terminal, after showing the pipeline's rules, `add` offers three choices: **install now**, **try silently first** (a shadow install — see below), or **quit**. Re-adding a name that is already installed never replaces it silently: `add` shows a line diff of the change (and the commit movement, for pinned sources), repeats that **you are responsible for what you pull and use**, and asks. Non-interactively (pipes, scripts, CI) nothing installs without an explicit opt-in: pass `--force` to accept the disclosure, or `--shadow` to try the pipeline without applying anything. The same rule applies to publishing: `share --gist` in a pipe needs `--force`, and the interactive publisher asks for an explicit "I understand" after its warning.

### Trying a pipeline silently (shadow installs)

`--shadow` (or answering `try silently` interactively) installs into a shadow slot: the pipeline is evaluated and **measured** on your live sessions but applied to nothing — no rewrites, no filtering, no denies, no tasks. After a few sessions, compare and decide:

```bash
rtk pipeline eval <name>          # what it would have done, vs your active pipeline, same traffic
rtk pipeline promote <name>       # go live (shows the diff against any installed version, confirms)
rtk pipeline promote <name> --discard   # drop it; nothing was ever applied
```

```text
installed: ~/.config/rtk/pipelines/flash.toml
auth needed: export GEMINI_API_KEY=...   (upstream `flash` — task `bulk-read`)
active now, merged with your other pipelines — see `rtk pipeline show`
```

The shared file names the *environment variable* for each key (`api_key_env`), never the key itself — you supply your own.

:::caution[You are responsible for what you install]
A pipeline directs your agent: it can rewrite the commands it runs, filter and replace what it reads, inject instructions, and send content to any upstream endpoint it declares. `add` prints exactly what the pipeline does — its rules, directives in full, and every upstream with the env var it would send — **before** anything runs. Read that summary, especially for a pipeline fetched from a URL.
:::

:::danger[Pipelines that need plugins run third-party native code]
Some pipelines reference **plugin** methods. A plugin is not sandboxed TOML — it is third-party native Rust that rtk compiles and runs on your machine with your privileges. If a shared pipeline needs a plugin you do not have, `add` names the missing plugin and refuses to install: to use it you must obtain the plugin's source from its author, add it to your build, and rebuild rtk. If the pipeline references a plugin you already have, `add` lists it in the disclosure. Either way, install a plugin only if you trust its author and have read its source — you alone are responsible for any native code you build and run.
:::

## Discovering shared pipelines

```bash
rtk pipeline search              # shared pipelines, most-starred first
rtk pipeline search rust         # filtered by a free-text query
```

Discovery is GitHub's own index of the `rtk-pipeline` topic. Each result shows the repo, its stars, and its description (which carries the author's headline number and the install line). The leaderboard is simply [github.com/topics/rtk-pipeline](https://github.com/topics/rtk-pipeline) sorted by stars. Descriptions and numbers are **self-reported by their authors** — rtk verifies nothing; `rtk pipeline add` shows every rule before anything installs, and `--shadow` lets you measure a pipeline on your own traffic before it touches anything.

## Evaluating a pipeline on your own usage

`eval` answers "would this pipeline do better on *my* workload?" without adopting it and without storing any conversation content. While armed, every tool output your sessions produce is also run through the candidate's transforms **in memory**; only the before/after sizes are recorded, the content is discarded, and your agent's output is untouched.

```bash
rtk pipeline eval squeeze-term --start   # arm the candidate (must be installed — see `add`)
# ...work normally for a few sessions...
rtk pipeline eval squeeze-term           # compare: candidate vs your active pipeline, same traffic
rtk pipeline eval squeeze-term --stop    # disarm
```

```text
Pipeline eval — `squeeze-term` · last 30 days · vs raw, same trajectory
  candidate      5.1K → 3.2K tokens  (37% removed · 214 outputs)   est.
  active now     5.1K → 4.0K tokens  (22% removed · 214 outputs)   est.
  ~ tokens est. ≈ bytes/4; both measured on the same live outputs
  ~ classifier/route effects excluded from the candidate
```

Both lines are measured on the **same** outputs, so the comparison is apples-to-apples. Limits, stated on the report itself: token counts are bytes÷4 estimates; a candidate's classifier- or route-driven rules are excluded (they would need live LLM calls), so eval covers its deterministic transforms.

The continuous counterpart lives in `rtk gain`: a per-mechanism efficiency section (filter+compress, LLM-spend, recalls, directives, proxy bill actual-vs-without) with a confidence tag on every line — `measured` where real provider usage exists, `est.` where it is a size estimate.

## Related commands

```bash
rtk methods list    # every registered method with its contract
rtk tasks list      # every configured task with its kind and upstream
```

---

### Guide Pipelines Conditions (`docs/guide/pipelines/conditions.md`)

---
title: Conditions and Judges
description: Every when key a rule can test, by position and subject — the agent's call context, proxy-safe keys, and AI classifiers (judges) that condition on meaning
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](./errors.md).

| 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](#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](#judges--conditions-on-meaning) read them:

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

[[hook.before_tool.read]]
name   = "big-reads-not-for-editing"
when   = { file_lines_gt = 400, whole_file = true, intent_matches = "(?i)understand|explain|overview" }
action = "route"
task   = "bulk-read"
```

## Proxy conditions

A block already in the conversation is part of the provider's cached prefix; if RTK treated it differently on one turn than on the previous one, the whole prefix would be re-billed. So a proxy rule may only look at things that are identical on every turn:

| Class | Keys | Legal on |
|---|---|---|
| block-intrinsic | `tool` `format` `shape` `tokens` `succeeded` | every part |
| session-stable | `model` `provider` `session` `project` | every part |
| turn-varying | `tokens_in` `message_count` `stream` | `user` and `any` only — rejected on `tool_result`, `tool_use`, `assistant`, `cache` (E010) |

`tool` at the proxy uses the same categories as the hooks (`shell`, `read`, `subagent` …). `shape` is not supplied by the daemon.

## Judges — conditions on meaning

A `[classifier.<name>]` asks a model a typed question and turns the answer into facts a rule conditions on. Three question kinds:

| `question` | Fact(s) produced | Tested with |
|---|---|---|
| `choice` | `class.<name>` (label) · `class.<name>.confidence` | `"class.intent" = "test_failure"`, `_in`, `.confidence_gte` |
| `score` | `score.<name>` (0–1) | `"score.quality_lt" = 0.4` — needs `rubric = [..]`, ordered worst to best |
| `noul` | `noul.<name>` (0–1 truth) | `"noul.is_error_gte" = 0.9` |

```toml
[upstream.jev]
provider     = "typesafe"
model        = "jev-latest"
base_url     = "https://api.typesafe.ai/v1/systemone"
api_key_env  = "TYPESAFE_API_KEY"
timeout_secs = 8

[classifier.intent]
upstream       = "jev"
question       = "choice"
instructions   = "What kind of tool output is this?"
input_chars    = 2000
min_confidence = 0.8

[classifier.intent.labels]
test_failure = "Failing test output: assertions, FAILED markers"
build_log    = "Compiler or build progress output"

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

[task.digest-failures]
kind     = "digest"
upstream = "flash"

[[hook.after_tool.shell]]
name   = "route-test-failures"
when   = { "class.intent" = "test_failure", tokens_gt = 1500 }
action = "route"
task   = "digest-failures"
```

| Key | Meaning |
|---|---|
| `upstream` | an [`[upstream.*]`](./tasks.md#upstreams) 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](#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](#call-context). That is what lets it tell output the agent wants to understand from output it needs verbatim. The call is sent in addition to `input_chars` of output, so the judge still sees as much output as before. Before a tool it reads the *call*: the tool, its command or path, size facts (`file_lines`, `file_bytes`), every `input.*` field — for Bash, the agent's own `description` of why it runs the command — the [call context](#call-context) and, for a read, the head of the file. The context is what separates "read to understand" from "read to edit", and it is what lets a judge delegate work before it is done. It costs one judge round trip per call it is asked about (cached afterwards), so put cheap fixed conditions in the same `when` (`file_lines_gt`, `command_matches`) and the judge is only asked about calls worth delegating.
- **Never on `deny`** (E020). Gate denies on fixed conditions.
- **Not under `proxy.request`.** Judges run at hook positions only.
- **Fail-open, always.** Missing key, endpoint down, timeout, or confidence under the floor: the fact is absent, absent facts never match, and the call or output goes through untouched.
- **Lazy, batched, memoized.** Nothing runs unless an enabled rule at that exact position conditions on a classifier; every question bound to one upstream travels in a single request; answers are cached per content, so repeated output classifies once.
- **Auth is declared, not discovered.** The upstream's `api_key_env` names the credential; `rtk pipeline validate`, `rtk trust` and `rtk init` print `auth needed: export VAR=...` for anything unset. See [Tasks and Upstreams](./tasks.md#upstreams) for `api_key_command` and for borrowing the key your agent already uses through the RTK Proxy.

### A judge before the tool

Delegate big reads, heavy commands and documentation fetches only when the judge thinks the agent needs a summary rather than the bytes:

```toml
[upstream.judge]
provider     = "laya"
model        = "laya"
base_url     = "http://127.0.0.1:41733/v1/systemone"
api_key_env  = "RTK_LAYA_TOKEN"
timeout_secs = 4

[upstream.haiku]
provider = "anthropic"
model    = "claude-haiku-4-5"

[classifier.skim]
upstream     = "judge"
question     = "noul"
instructions = "The agent is about to run this tool call. Use its intent, the user goal and its recent calls. Answer true if a short answer a smaller model could extract is enough, false if it needs the exact content."
input_chars  = 1500

[task.bulk-read]
kind     = "summarize"
upstream = "haiku"

[[hook.before_tool.read]]
name   = "judge-big-reads"
when   = { file_lines_gt = 300, whole_file = true, "noul.skim_gte" = 0.8 }
action = "route"
task   = "bulk-read"
```

The full file, which also judges heavy commands and documentation fetches, ships as the `judge-before-tool` example pipeline; `classify-routing` is the after-tool counterpart.

For any AI-driven decision, use a classifier.

---

### Guide Pipelines Directives (`docs/guide/pipelines/directives.md`)

---
title: Directives
description: Text RTK puts in front of the agent — shipped directives, your own, how they land in the instruction file and what they cost
sidebar:
  order: 5
---

# Directives

A directive is text RTK writes in front of the agent. It is how RTK reaches what no transform can touch: how the agent writes, which commands it uses.

```toml
[[directive]]
use = "terse-output"

[[directive]]
name = "house-rules"
file = ".rtk/directives/house-rules.md"

[[directive]]
use   = "rtk-awareness"
level = "default"
```

| Key | Notes |
|---|---|
| `use` / `text` / `file` | exactly one: a shipped directive, inline text, or a file relative to the pipeline file |
| `name` | defaults to `use` |
| `channel` | `awareness_file` (default): RTK's instruction file, present every turn |
| `enabled` | |
| module options | `level` for `rtk-awareness` |

## Shipped

| Directive | Ships | What it does |
|---|---|---|
| `rtk-awareness` | on | the awareness levels (`default` · `high` · `full`); `[awareness]` in `config.toml` still applies |
| `terse-output` | off | terse prose; code, paths, commands and errors are never abbreviated, and destructive-action warnings stay full sentences |
| `route-tasks` | generated | written whenever a task has an upstream: tells the agent the exact `rtk <task>` syntax |

A file containing nothing but a directive is a valid pipeline and leaves `rtk:default` intact — see [`rtk:terse`](./reference-pipelines.md#rtkterse--terse-prose).

## 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
description: Every pipeline diagnostic code — what fails closed, what degrades — plus the engine's limits
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](./commands.md#validating-your-own-pipelines).

## 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`](./writing-pipelines.md#route-modes) 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](./writing-pipelines.md#routing-before-a-tool)) |
| W024 | a rule field has the wrong type (`enabled = "false"`, `action = 5`) — the rule is disabled; the rest of the file still loads |
| W025 | a pipeline file has errors and is not loaded; the other files stay active (the file's own errors are listed with it) |

## Limits

- **No file contains a command to run.** Rewrite is fixed to the `rtk` prefix; tasks call a model, not a shell. Cloning a repository cannot make RTK execute anything.
- **No file names an agent.** Subjects are categories; RTK maps them at `rtk init`.
- `proxy.*` rules need the RTK Proxy in the path. Without it they are inert and `validate` says so (W005).
- A `route` rule needs its task bound to an upstream.
- There are two scopes: you and the project.

---

### Guide Pipelines Methods (`docs/guide/pipelines/methods.md`)

---
title: Methods
description: The methods RTK composes pipelines from, and what each one costs and recovers
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/compression.md#your-own-methods-on-tool-results).

## Proxy-resident

Implemented in the [RTK Proxy](../proxy/overview.md) 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](../proxy/compression.md#large-tool-results-ccr) |
| `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](../proxy/compression.md#large-tool-results-ccr) |

Naming it under `hook.*` leaves the chain without it, so the rule fires as a
no-op. `validate` reports **W011**, and **W012** when that empties the rule
completely; `rtk pipeline show` marks the rule `(inert: proxy-only)`.

## Chains — shipped composites, usable anywhere a method is

| Chain | = |
|---|---|
| `term` | `ansi` `progress` `dedup` `base64-elide` `cap` |
| `json` | `base64-elide` `toon` |
| `web` | `ansi` `base64-elide` `web-md` `toon` `md-slim` |
| `matches` | `grep-group` |

## Your own chains

```toml
[method.gentle]
chain = ["ansi", "dedup"]

[[hook.after_tool.shell]]
name = "gentle-term"
use  = ["gentle"]
```

Chains are also composed directly in rules — `use = ["ansi", "cap"]` runs one
then the other. Order is yours and is checked.

## Your own command filters

Custom command filters live where they always have: `~/.config/rtk/filters.toml`
(macOS `~/Library/Application Support/rtk/filters.toml`) and `.rtk/filters.toml`
in a project. Nothing about them changes in v1.0 — same keys, same inline
tests, same `rtk verify`.

```toml
schema_version = 1

[filters.mytool]
description          = "Output of my internal deploy tool"
match_command        = "^mytool\\b"
strip_ansi           = true
strip_lines_matching = ["^\\[INFO\\]", "^Downloading "]
max_lines            = 200
on_empty             = "(no output)"

[[tests.mytool]]
name = "strips info"
input = "[INFO] x\nreal line"
expected = "real line"
```

Then run `rtk trust` once. Filter files are trust-gated because they rewrite
what your agent sees; an untrusted (or edited-since-trust) file is skipped.

For a command RTK has no filter for, the `rtk-rewrite` rule rewrites
`mytool deploy` → `rtk mytool deploy` (it consults every filter's
`match_command`), and `rtk mytool deploy` runs the real command and applies your
filter. Filters are the rewrite's business: they are not methods and do not
appear in `use`, and the pipeline file does not define them (E016). The
decision log and `rtk pipeline explain --last` show the rewrite that happened.

## Your own methods in Rust

A `[method.*]` composes existing methods; a [plugin](./plugins.md) 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
description: Write your own pipeline method in Rust and build it into rtk — the Method trait, the rtk-plugin SDK, and the rtk plugins workflow
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.*]`](./conditions.md#judges--conditions-on-meaning) in your `pipeline.toml`, not a
plugin. Plugins are for *transforming* a block of text.

## The workflow

```bash
rtk plugins new shout      # scaffold a method crate in ~/.config/rtk/plugins/
# edit shout/src/lib.rs — fill in apply()
rtk plugins build          # compile a custom rtk with your method linked in
rtk plugins list           # show your plugin crates and what this binary carries
```

Then name the method in a rule, exactly like a built-in:

```toml
[[hook.after_tool.shell]]
name = "loud-errors"
use  = ["shout"]
```

`rtk plugins build` generates a small host crate that depends on `rtk` and your
plugin crates, registers each method, and calls `rtk::entry()`; it compiles with
`cargo` and installs the result (a binary still named `rtk`) to `~/.cargo/bin`.
Nothing about the engine, the hooks, or installation changes — a plugin is
purely additive.

## What you write

A method implements one trait from the `rtk-plugin` SDK. The scaffold gives you
an identity transform to fill in:

```rust
use rtk_plugin::{rtk_plugin, Block, Method, MethodCtx, MethodInfo, Outcome};

struct Shout;

impl Method for Shout {
    fn name(&self) -> &'static str {
        "shout"
    }

    fn info(&self) -> MethodInfo {
        MethodInfo::LOSSLESS_TRANSFORM
    }

    fn apply(&self, block: &Block, _ctx: &MethodCtx) -> Outcome {
        Outcome::Continue(block.text.to_uppercase())
    }
}

rtk_plugin!(Shout);
```

- `apply` receives the block's `text` (plus its subject, format, and provenance)
  and returns an `Outcome`: `Continue(String)` to pass the transformed text down
  the chain, `ShortCircuit(String)` to stop the chain, or `Skip` to leave the
  block untouched.
- `info()` is the contract the engine type-checks at load: `lossy`, `recovery`
  (how a reader gets the original back, e.g. `Recall` or `None`), `cost`, and what the method consumes/produces. Use
  `MethodInfo::LOSSLESS_TRANSFORM` as the base and override only what differs —
  a lossy method must declare its recovery so the original can be restored:
  by default from the recall store (`rtk recall <hash>`), or from files under
  the tee directory if you set `[retriever] mode = "tee"`.
- `rtk_plugin!(…)` lists what the crate exports so the build can register it.
  A crate may export several methods: `rtk_plugin!(Shout, Whisper);`. To export
  task kinds too (or instead), use the long form:
  `rtk_plugin!(methods: [Shout], kinds: [Checklist]);` — `rtk plugins build`
  registers both.

`rtk pipeline validate` checks a plugin method like any other — a bad contract
(a terminal method not last, a `Slow` method at PreToolUse, a format it cannot
consume) fails at build, with the method name.

## Your own task kinds

Methods transform content; **task kinds** define what a delegated worker does.
The built-ins are `summarize` (answer a question about files), `generate`
(write one file from a spec), and `digest` (condense command output). A plugin
can register a new kind the same way it registers a method:

```rust
use rtk_plugin::{rtk_plugin, CorpusFile, TaskIntent, TaskKind, TaskOutcome, WorkerRequest};

struct Checklist;

impl TaskKind for Checklist {
    fn name(&self) -> &'static str { "checklist" }
    fn usage(&self) -> &'static str { "rtk <task> --question \"...\" <files>" }
    fn default_instructions(&self) -> &'static str { "Answer as a checklist." }
    fn assemble(&self, intent: &TaskIntent, corpus: &[CorpusFile],
                instructions: Option<&str>, prompt: Option<&str>)
        -> Result<WorkerRequest, String> { /* build the worker prompt */ }
    fn handle(&self, output: &str) -> TaskOutcome {
        TaskOutcome::Print(output.trim().to_string())
    }
}

rtk_plugin!(methods: [], kinds: [Checklist]);
```

The `rtk_plugin!` line is what `rtk plugins build` wires into the binary. Once registered,
`[task.review] kind = "checklist"` validates, `rtk review`
exists as a CLI command, and rules can `action = "route"` to it. Names must be
unique — built-in kinds cannot be shadowed. A pipeline that declares a task
with a kind your build does not have fails validation (E014).

## Upgrading rtk

A plugin is compiled into your custom `rtk` binary, so installing a new rtk
release replaces that binary and your plugins with it. After every upgrade:

1. In each plugin's `Cargo.toml`, set the `rtk-plugin` dependency's `tag` to
   the new version (`rtk --version` → `vX.Y.Z`). `rtk plugins new` writes the
   tag once, at scaffold time.
2. Run `rtk plugins build` again.

Until you rebuild, a pipeline rule that uses a plugin method fails open: it gets
W014 and is skipped while the rest of the file stays active. A control rule (one
with `action =`) that depends on it is an error (E007) and its file is not
loaded. `rtk pipeline validate` shows which rules are affected.

## Requirements and notes

- A Rust toolchain (`cargo`) on PATH, and network access on the first build —
  the scaffolded `Cargo.toml` depends on `rtk-plugin` by git, pinned to the tag
  matching your installed `rtk`, so your method always links the exact engine
  your `rtk` runs.
- A plugin adds methods and task kinds, not conditions. Every field of a
  tool's input is already a condition via the `input.*` prefix
  (`when."input.subagent_type" = "Explore"`), and AI-driven conditions are
  classifiers.
- A project-local `.rtk/plugins/` is trust-gated like every other project file;
  review with `rtk trust` before it is built.

---

### Guide Pipelines Reference Pipelines (`docs/guide/pipelines/reference-pipelines.md`)

---
title: Reference Pipelines
description: The six built-in pipelines as literal files — rtk:default, rtk:posthook, rtk:proxy, rtk:delegate-reads, rtk:verbatim-diffs, rtk:terse
sidebar:
  order: 7
---

# Reference Pipelines

Printable with `rtk pipeline show <name>`. Adopt with `extends`; tune with [`[override]`](./writing-pipelines.md#overrides).

## `rtk:default` — RTK exactly as it works today

What every user has after upgrading, with no action. One hook event (`PreToolUse`), the awareness file, your `filters.toml` and `config.toml`. Nothing else is registered or changed.

It also carries one `proxy.request` rule, `normalize`, which does nothing unless you are running the daemon. It strips ANSI codes, progress bars and repeated lines — bytes no reader ever sees — and each of its methods is a pure function of the block's own content, so the request prefix stays byte-identical across turns and the upstream prompt cache keeps hitting.

```toml
schema_version = 1
[pipeline]
name = "rtk:default"

[[hook.before_tool.shell]]
name   = "rtk-rewrite"            # `ls` → `rtk ls` for every command RTK has a filter for; others untouched
action = "rewrite"

[[proxy.request.tool_result]]
name = "normalize"                # inert without a running daemon
use  = ["ansi", "progress", "dedup"]

[[directive]]
use = "rtk-awareness"
level = "default"     # today's [awareness] level in config.toml still applies

[guards]
never_worse = true
min_size = 512
```

How the rewrite flows: your agent calls `Bash "ls -la"` → the hook evaluates `hook.before_tool.shell` → `rtk-rewrite` matches → the call becomes `rtk ls -la` → inside `rtk`, the `ls` filter (built-in, or yours from `filters.toml`) applies. Excluding a command is a `passthrough` rule ahead of `rtk-rewrite`, or `[hooks] exclude_commands` in `config.toml` as before.

**Hooks follow the pipeline.** `rtk init` registers only the events your active pipeline uses, for the tools its rules name. With `rtk:default` that is `PreToolUse` on `Bash`, as today. Enable `rtk:posthook` and `PostToolUse` is registered too. Registration happens at `rtk init`: after adding a rule at a new event or for a new tool (a `hook.before_tool.read` rule, any `hook.after_tool` rule), run `rtk init -g` again. Until you do, `rtk pipeline validate`, `rtk gain` and `rtk init --show` print `hook needed: …`.

## `rtk:posthook` — compress every tool's output

Opt in. Adds `PostToolUse`: after a tool runs, its output is compressed before the agent reads it. Only representation changes; nothing is dropped except by `cap`, which keeps the full output for `rtk recall`.

```toml
[pipeline]
name = "rtk:posthook"
extends = "rtk:default"

[[hook.after_tool.shell]]
name = "term-default"             # floor for output that did not come through rtk
when = { is_rtk_command = false }
use  = ["term"]

[[hook.after_tool.read]]
name = "lockfiles"
when = { basename_in = ["package-lock.json", "Cargo.lock", "yarn.lock", "pnpm-lock.yaml"] }
use  = ["lockfile"]

[[hook.after_tool.read]]
name = "json-reads"
enabled = false
when = { format = "json" }
use  = ["json"]

[[hook.after_tool.read]]
name = "strip-line-numbers"
enabled = false
use  = ["strip-line-numbers"]

[[hook.after_tool.search]]
name = "grep-group"
when = { format = "matches" }     # grep output; glob file lists are not matches and pass untouched
use  = ["matches"]

[[hook.after_tool.web]]
name = "web-default"
use  = ["web"]
```

## `rtk:proxy` — cache alignment in the proxy

Opt in; needs the RTK proxy running (`rtk init` offers to install it). Normalisation
(`ansi`, `progress`, `dedup`) is inherited from `rtk:default`, so what this pipeline adds on top
is the cache breakpoint.

Both are **Anthropic-dialect only**, and `cache-align` does nothing for a client
that already sets its own `cache_control` breakpoints — Claude Code does. An
OpenAI-dialect client is measured but not compressed ([Provider dialects](../proxy/configuration.md#provider-dialects)).

```toml
[pipeline]
name = "rtk:proxy"
extends = "rtk:default"

[[proxy.request.cache]]
name = "align"
use  = ["cache-align"]
```

## `rtk:delegate-reads` — route oversize reads to a cheaper model

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

[task.bulk-read]
kind = "summarize"    # no upstream: you supply the model
[task.code-write]
kind = "generate"

[[hook.before_tool.read]]
name   = "shunt-large-read"
when   = { file_lines_gt = 350, whole_file = true }
action = "route"
task   = "bulk-read"
```

Adopt it as in [the third example on the overview](./overview.md#three-files-you-might-write). 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](./writing-pipelines.md#routing-before-a-tool)). The same action covers the other two
anchors. Neither is shipped in a built-in — write them yourself:

```toml
[pipeline]
name = "delegate-commands"
extends = "rtk:posthook"

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

[task.check-tests]
kind     = "digest"
upstream = "flash"

[[hook.before_tool.shell]]           # the command never runs in your context
name   = "delegate-tests"
when   = { command_matches = "^(npm|pnpm) test\\b" }
action = "route"
task   = "check-tests"

[[hook.after_tool.web]]              # the fetch already happened; digest it
name   = "digest-pages"
when   = { tokens_gt = 4000 }
action = "route"
task   = "check-tests"
```

The first denies the call and tells the agent to run
`rtk check-tests --question "…" -- npm test`; the exit code still comes from
`npm test`. The second sends the fetched page to the worker and returns its
bullets with a `rtk recall` hint for the original.

Prefer a transform chain where one is enough. `use = ["term"]`
costs no time and no tokens and keeps the whole output recoverable; an
`after_tool` route spends a provider round trip — seconds, on the hook path —
to throw detail away. It earns its keep on output that is both very large and
genuinely disposable, and `timeout_secs` is what bounds the damage when the
worker is slow.

## `rtk:verbatim-diffs` — no line numbers, diffs untouched

```toml
[pipeline]
name = "rtk:verbatim-diffs"
extends = ["rtk:posthook", "rtk:proxy"]

[override.strip-line-numbers]     # for agents whose edit tools do not address lines by number
enabled = true

[[hook.before_tool.shell]]        # diffs must reach the agent byte-for-byte
name   = "keep-diffs"
when   = { command_matches = "^git (diff|show)\\b" }
action = "passthrough"
```

Selective compression with a recovery path and grep grouping come from `rtk:posthook` (`term`, `matches`). It also inherits `rtk:proxy`, which adds the cache breakpoint (see [`rtk:proxy`](#rtkproxy--cache-alignment-in-the-proxy)).

## `rtk:terse` — terse prose

```toml
[pipeline]
name = "rtk:terse"
extends = "rtk:default"

[[directive]]
use = "terse-output"
```

A directive only: it changes what the agent *writes*, never what it reads, so it
composes with anything. For terse prose *and* output compression, name both —
`extends = ["rtk:terse", "rtk:posthook"]`. A directive is also the one thing you
can adopt without a pipeline at all: a file containing nothing but
`[[directive]] use = "terse-output"` is valid and leaves `rtk:default` intact.

---

### Guide Pipelines Tasks (`docs/guide/pipelines/tasks.md`)

---
title: Tasks and Upstreams
description: Delegate reads, commands and writes to a cheaper model — task kinds, instances, upstream endpoints, keys, and what it costs
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](./plugins.md) 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 "…" <paths…>` | read |
| `digest` | captured output + a question → bullets | `rtk <task> --question "…" -- <command>` | shell, search, web, mcp |
| `generate` | a spec + a reference file → a file written; the agent sees one line | `rtk <task> --spec "…" --reference <f> --target <f>` | write, read |

```toml
[upstream.flash]
provider     = "google"
model        = "gemini-2.5-flash"
api_key_env  = "GEMINI_API_KEY"
timeout_secs = 30

[task.bulk-read]
kind         = "summarize"
upstream     = "flash"
temperature  = 0.2

[task.code-write]
kind     = "generate"
upstream = "flash"
```

The agent reaches a task two ways: a `route` rule hands it a call ([Routing before a tool](./writing-pipelines.md#routing-before-a-tool)), or the generated `route-tasks` [directive](./directives.md) 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](./writing-pipelines.md#routing-after-a-tool), or a [route mode](./writing-pipelines.md#route-modes) `swap` or `answer` — nobody wrote one. With `context = true` the worker gets the agent's [call context](./conditions.md#call-context), so it keeps what the agent was looking for:

```text
Question: The user asked: fix the failing build
The agent's current step: find which module fails and why
Its recent tool calls: shell mvn -q package | read pom.xml
What in this does the agent need for its current step? Keep exact names, numbers, paths, line numbers and error lines.
```

Without it, the worker gets only the last line. The context costs a few hundred input tokens per call. On Claude Code the agent's current step can lag one step behind, because the transcript is written asynchronously; the user's request and the recent calls are always current.

**Personas, not kinds:** a "review" task is `[task.review] kind = "summarize" instructions = "…"`. New personas are three lines; new kinds are [plugins](./plugins.md).

## Upstreams

An upstream is a model endpoint, shared by tasks and [judges](./conditions.md#judges--conditions-on-meaning).

| 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](../proxy/overview.md) 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
description: Where pipeline files live, the rule tree (site, event, subject), rule anatomy, actions, routing to a cheaper model, and overrides
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](./reference-pipelines.md) |
| `~/.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](./methods.md#your-own-command-filters) |

Paths use the Linux spelling. The user directory is the platform config dir: `~/.config/rtk/` on Linux, `~/Library/Application Support/rtk/` on macOS, `%APPDATA%\rtk\` on Windows.

**Load order:** built-ins → your `pipeline.toml` → your `pipelines/` → the project's `pipeline.toml` → the project's `pipelines/`. Later wins. One file is the simple case; a directory is how you keep several pipelines and turn them on by dropping them in.

**Across files:** definitions (`[task]`, `[upstream]`, `[method]`) merge by key; `[override]` applies later-wins per field; a rule or directive name that appears in two loaded files is an error (E013) naming both paths — nothing shadows silently, and `rtk pipeline show` prints the source file of every line. Files do not extend each other: they are all loaded, so to reuse a piece, give it its own file.

`proxy.request` rules belong in your user files: one daemon serves every project, so it never reads a project's `.rtk/` pipeline (W017).

`config.toml` is unchanged and separate: preferences (`[tracking]`, `[display]`, `[hooks]`, `[retriever]`, `[telemetry]`, `[limits]`, `[awareness]`) stay there. The pipeline holds behaviour; it never replaces an existing RTK file.

### Layout

```text
schema_version = 1
[pipeline]                         # name, extends, enabled

[[hook.before_tool.<subject>]]     # rules — before a tool runs
[[hook.after_tool.<subject>]]      # rules — after a tool ran
[[proxy.request.<part>]]           # rules — the request, about to be sent

[[directive]]                      # what RTK tells the agent
[method.<name>]                    # chains, options for built-in methods
[classifier.<name>]                # AI judges rules can condition on
[task.<name>]                      # delegation commands
[upstream.<name>]                  # model endpoints
[override.<name>]                  # patch an inherited rule or directive
[guards]                           # global safety limits
```

A minimal file uses one or two of these.

### `[pipeline]`

| Key | Value |
|---|---|
| `name` | optional for your files; `rtk:*` for built-ins |
| `extends` | a built-in name or a list of them: `"rtk:default"`, `["rtk:delegate-reads", "rtk:terse"]`. Left to right; a shared ancestor is inherited once; the same rule name from two parents is an error unless the rules are identical. Built-ins only — local files compose by being loaded, not by extending each other. Omit for a standalone file. |
| `enabled` | `false` turns the whole file off without moving it. Default `true`. |

## The tree — where a rule lives

A rule's position says three things, in this order:

| Segment | Answers | Values |
|---|---|---|
| **site** | which RTK mechanism runs it | `hook` · `proxy` |
| **event** | when | `before_tool` · `after_tool` · `request` |
| **subject** | on what | a tool category (hook) · a part of the request (proxy) |

The position fixes what the rule can see and do. `file_lines` exists before a tool runs (the file is on disk); `format` exists after (there is output). A rule that names a condition its position cannot provide does not load — see [Conditions and Judges](./conditions.md).

### 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](../resources/agent-coverage.md) 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](../proxy/compression.md).

### The token layers, for reference

If you think in the layers Claude Code's `/context` shows, this is where each is configured. Layers are how `rtk gain --by-layer` reports; they are never written in the file.

| Layer | Where |
|---|---|
| Tool results | `hook.after_tool.{shell,search,web,subagent,mcp}` (`rtk:posthook`) · `proxy.request.tool_result` |
| Read | `hook.before_tool.read` · `hook.after_tool.read` |
| Write | `proxy.request.tool_use` · the `code-write` task |
| History | `proxy.request.tool_result` · `proxy.request.assistant` |
| Cache | `proxy.request.cache` |
| Agent message | directive `terse-output` |
| Skills / MCP · User prompt | directives |
| Inference | `route` to a task before any tool, or after any tool with extractable output, optionally gated on a judge |

## Rules

### Anatomy

```toml
[[hook.after_tool.shell]]
name     = "term-default"
when     = { is_rtk_command = false }
use      = ["term"]
continue = false
enabled  = true
shadow   = false
```

| Key | Required | Notes |
|---|---|---|
| `name` | yes* | unique across the resolved pipeline. *Defaults to the first `use` entry when that is unambiguous; a derived name that collides with another rule is a warning — set it explicitly |
| `when` | no | conditions, all must hold; omitted = matches everything |
| `use` / `action` | exactly one | `use` is a chain of [methods](./methods.md); `action` is control flow (below) |
| `task` · `upstream` · `message` | with `route` / `deny` | see [Actions](#actions) |
| `mode` | no | `route` before a tool only: `suggest` (default) · `swap` · `answer` — see [Route modes](#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](./commands.md#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](./tasks.md)). 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 <url>` | `digest` |
| `search` | `-- rg -n '<pattern>' <path>` | `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](./conditions.md#judges--conditions-on-meaning) to delegate only the calls worth it.

### Route modes

By default a route before a tool *suggests* the task, and the agent spends a turn running it. `mode` lets the rule deliver the cheaper answer in the same turn instead:

| `mode` | What happens | Extra turn | Works on |
|---|---|---|---|
| `suggest` *(default)* | the call is refused with a message naming the task; the agent decides to run it | yes | every subject, every agent |
| `swap` | the command is replaced by the task wrapping it — `rtk <task> --question "…" -- sh -c '<command>'` — and runs as the agent's own call | no | `shell` |
| `answer` | RTK runs the task itself and gives the agent the answer in place of the call | no | `read`, `search`, `web` |

```toml
[upstream.haiku]
provider = "anthropic"
model    = "claude-haiku-4-5"

[task.bulk-read]
kind     = "summarize"
upstream = "haiku"
context  = true

[task.check-build]
kind     = "digest"
upstream = "haiku"
context  = true

[[hook.before_tool.read]]
name   = "answer-big-reads"
when   = { file_lines_gt = 400, whole_file = true }
action = "route"
task   = "bulk-read"
mode   = "answer"

[[hook.before_tool.shell]]
name   = "swap-builds"
when   = { command_matches = "^mvn " }
action = "route"
task   = "check-build"
mode   = "swap"
```

With `swap` and `answer` there is no agent writing the question, so RTK asks one. With [`context = true`](./tasks.md#tasks) on the task, it is built from what the agent is doing — the user's request, the agent's current step and its recent calls. Without it, a generic question asks for what matters, keeping exact names, numbers and error lines.

- **`swap`** needs nothing from the hook: the agent's own call runs the task, and the command keeps its exit code. If the worker fails, the command's own output is shown. Agents that cannot replace a command show the swapped command as a suggestion instead.
- **`answer`** waits for the worker inside the hook, bounded by the upstream's `timeout_secs` — set it low. If anything fails (no key, timeout, empty answer), the original call runs as if the rule had not matched. After an answered read the agent can still read exact lines with `offset` and `limit`, since a ranged read is not `whole_file`.
- Any other combination is refused (E005): `swap` is for commands only, and `answer` on a shell command is what `swap` does without blocking the hook.

### Routing after a tool

On any subject with extractable output the output already exists, so there is nothing to ask the agent to re-run: RTK sends the output to the task and puts the answer in its place, with the original kept for `rtk recall`. The task must be a `digest` instance. Set [`context = true`](./tasks.md#tasks) on it so the worker knows what the agent was looking for; otherwise it answers a generic question.

This is the one rule that makes a provider call on the hook path — bound it with the upstream's `timeout_secs` (default 30 s; set it lower for the hook path). Every failure — no upstream, refused, timed out, empty answer — passes the original output through untouched.

`route` after a `read` is allowed but warned (W021): a later `Edit` needs the exact bytes, so keep it to files the agent only reads. After `write` and `edit` there is no output to replace (W013).

## Overrides

Everything you inherit has a name. `[override.<name>]` patches one inherited rule or directive in place; fields you do not mention are kept.

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

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

[override.strip-line-numbers]
enabled = true

[override.term-default]
use = ["ansi", "dedup"]

[override.rtk-awareness]
level = "full"
```

| Field | Semantics |
|---|---|
| `enabled` `action` `task` `upstream` `message` `continue` `shadow` and module options | replaced |
| `when.<key> = v` | patches one key; the rest of `when` is kept |
| `when = { … }` | replaces the whole condition — `validate` warns and lists the keys you dropped (W003) |
| `use` | replaced whole |
| `name`, position in the tree | not overridable — that is a new rule |

Overriding a name that does not exist (E011), or declaring a new rule with an inherited name (E012), does not load. Project overrides beat user overrides beat built-ins. `rtk pipeline show` prints where each value came from.

**Add vs change:** to change how an inherited rule behaves, override it. To add behaviour, write a new rule with a new name — it is evaluated before the inherited ones and can shadow them for what it matches.

---

### Guide Proxy Compression (`docs/guide/proxy/compression.md`)

---
title: What the Proxy Compresses
description: The pipeline rules the RTK Proxy runs on each request, what ships on, and how to add your own methods on tool results
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](../pipelines/writing-pipelines.md#proxy-parts) 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](../pipelines/conditions.md#proxy-conditions).

## 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](./configuration.md#provider-dialects).

## Your own methods on tool results

The daemon evaluates your `proxy.request.tool_result` rules per tool result — rule order and `when` honoured, conditions limited to the cache-safe keys — and runs the selected chain. `web-md`, `md-slim`, `base64-elide`, `lockfile` and your own `[method.*]` chains of them work there.

```toml
[[proxy.request.tool_result]]
name = "slim-web-pages"
when = { tool = "web" }
use  = ["web-md", "md-slim"]
```

Two limits apply:

- **`Read` results are never rewritten** — a later `Edit` needs the bytes on disk.
- **Methods whose only recovery is a recall copy are skipped** (`cap`, `toon`, `minify-json`, `rle`, `grep-group`, `truncate`): the daemon cannot store that copy. Use them under `hook.after_tool` instead. `rtk pipeline validate` reports W016 for a method that will be skipped, and for an engine method placed at a part the daemon does not transform (`tool_use`, `assistant`).

## Large tool results: CCR

Off until a rule names it. `ccr` compresses a tool result that is already in the conversation: the block becomes a head-and-tail preview ending in a marker,

```text
[rtk-proxy: compressed N tokens — call rtk_retrieve("<id>") to get the full content]
```

and an `rtk_retrieve` tool is added to the request. When the model calls it, the daemon answers from its own store inside the same request, so your agent never sees the round trip. The marker is identical on every later turn, so the cached prefix is not disturbed.

```toml
[[proxy.request.tool_result]]
name = "ccr"
when = { tokens_gt = 1500 }
use  = ["ccr"]
```

Name the rule `ccr`: its `tokens_gt` is the floor below which a result is left alone (1500 tokens if the rule has none). `Read` results are never compressed. `RTK_PROXY_CCR=false` keeps the method off even when a rule names it.

**What it stores.** To answer `rtk_retrieve`, the daemon keeps the original text of every result it compressed in `rtk-proxy-memory.db`, next to the usage database and readable by your user only, for 7 days (`icm_ttl_secs`). This is the one case where the proxy keeps request content on disk. Delete the file to wipe it; the daemon recreates it empty.

`retire-pointer`, at `proxy.request.tool_use`, is the companion for a file body echoed back through a `Write` or `Edit` argument: the write already succeeded, so the argument becomes a pointer to the file and the agent's own `Read` is the recovery. Same store, same marker; a rule named `retire-writes` sets its floor (400 tokens if none).

---

### Guide Proxy Configuration (`docs/guide/proxy/configuration.md`)

---
title: Proxy Configuration
description: RTK Proxy settings, supported provider dialects, the HTTP endpoints it answers, and its attribution headers
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](./compression.md#large-tool-results-ccr)); `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](../pipelines/tasks.md)) |

```bash
rtk llm-proxy config get
```

The upstreams cannot be set over HTTP; `cache_align` and `normalize` are boot-only. A pipeline change takes effect on restart (`rtk llm-proxy stop && rtk llm-proxy start`) — the daemon does not watch the file.

There is no body-logging switch: the daemon does not store prompt or completion text. The one exception is opt-in — CCR keeps the originals it compresses, see [Large tool results](./compression.md#large-tool-results-ccr).

:::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
description: Exactly which parts of a pipeline work on each supported agent, and where the agent itself is the limit
sidebar:
  order: 6
---

# Agent Coverage

A `pipeline.toml` is written once. How much of it actually runs depends on what your agent's hook API lets RTK do — and that varies a lot.

Where a column says "—", the agent does not offer the capability.

## The table

Rows are grouped by what works — output compression first, then command rewriting without compression, then partial coverage — and alphabetically within each group.

| Agent | Rewrite the command | Block it | Compress tool output | Add context | Session start | Route via proxy |
|---|---|---|---|---|---|---|
| **Claude Code** | yes | yes | **yes** | yes | yes | yes |
| **Copilot CLI** | yes | yes | **yes** ¹ | yes | yes | yes |
| **Hermes** | yes | yes | **yes** | yes | yes | yes |
| **Kilo Code** ⁸ | yes | yes | **yes** | yes | — | yes |
| **Mistral Vibe** | yes | yes | **yes** ³ | yes | — | — |
| **Oh My Pi** | yes | yes | **yes** | yes | yes | yes |
| **OpenCode** | yes | yes | **yes** ² | yes | — | — |
| **Pi** | yes | yes | **yes** | yes | yes | — |
| **Cursor** | yes | yes | MCP only | yes | yes | — |
| **Codex** | yes ⁵ | yes | — | yes | yes | yes |
| **Copilot (VS Code)** | yes | yes | — | yes | yes | — |
| **Factory Droid** | yes | yes | — | yes | yes | — |
| **Gemini CLI** | yes ⁴ | yes | — | yes | yes | yes |
| **OpenClaw** | yes | yes | — | — | yes | yes |
| **Trae** | yes | yes | — | yes | yes | — |
| **Windsurf / Devin** ⁸ | yes ⁶ | yes | — | yes | yes | — |
| **Antigravity** ⁸ | — | yes | — | — | — | — |
| **Cline** ⁸ | — ⁷ | yes | — | yes | — | yes |
| **Kimi Code** ⁸ | — | yes | — | — | yes | yes |
| **Roo Code** | — | — | — | — | — | yes |

**Command rewriting — turning `git status` into `rtk git status` — is allowed by 16 of 20; `rtk init` installs it on 14.** That is RTK's main mechanism, and it is where most of the savings come from.

**Compressing tool output after the fact is allowed by 8; `rtk init` installs it on 7.** Everywhere else the model reads the raw result, because the agent provides no way to replace it.

## Footnotes — where the detail matters

1. **Copilot CLI**: output replacement relies on Copilot CLI's documented `modifiedResult` field.
2. **OpenCode**: built-in tools only. MCP results are rebuilt after the hook runs, and built-in output is already truncated by OpenCode before RTK sees it.
3. **Mistral Vibe**: replacement rides on a `deny` decision, reaches only the text form of the result, and the UI shows it as a warning.
4. **Gemini CLI**: input edits merge rather than replace, and each one makes Gemini append a `[System] Tool input parameters were modified` note to the output — so rewriting costs a few tokens back.
5. **Codex**: the rewrite is honoured only alongside an explicit `allow` decision. Without it the hook fails and the original command runs unwrapped.
6. **Windsurf**: now **Devin Desktop**. Older Windsurf installs are block-only and cannot rewrite.
7. **Cline**: the VS Code extension silently drops input overrides and cannot replace output. Both work only in the SDK/CLI plugin runtime, which is a different install. Hooks are also off by default — enable them in Feature Settings.
8. **Rules file only**: for Kilo Code, Cline, Windsurf / Devin, Kimi Code and Antigravity, `rtk init` writes a rules file asking the model to prefer `rtk <cmd>` and installs no hook. Their row shows what the agent's hook API allows; with RTK today, nothing is blocked, rewritten or compressed.

## What this means per pipeline position

| Position | Where it runs |
|---|---|
| `hook.before_tool.shell` | every agent with a hook |
| `hook.before_tool.{write,edit,search,web,subagent,mcp,any}` | every agent with a hook |
| `hook.before_tool.read` | Claude Code |
| `hook.after_tool.*` | Claude Code |
| `proxy.request.*` | any agent you can point at the daemon (the last column above) |

A non-shell rule can `deny` or `passthrough`. It cannot rewrite the call — that
is a `shell` capability, because rewriting a command is what RTK does.

Each subject sees the facts its conditions need, plus `input.*` mirroring the
tool's own arguments so a rule can reach a field RTK has no named condition for:

```toml
[[hook.before_tool.edit]]
name    = "no-lockfile-edits"
action  = "deny"
message = "do not hand-edit lockfiles"
when    = { basename = "Cargo.lock" }

[[hook.before_tool.mcp]]
name   = "one-server-only"
action = "deny"
when   = { tool_starts_with = "mcp__payments__" }
```

The `tool` fact is the *category* — `edit`, `web`, `search` — so one rule covers
agents that spell the tool differently. Under `mcp` and `any` it is the raw tool
name instead, which is what lets a rule target a single MCP server.

`read` keeps its own evaluator because its conditions (`file_exists`,
`file_bytes`, `file_lines`) are read from disk before the tool runs.

## Agents that cannot be made agnostic

Three are limited by the agent's own hook API:

- **Antigravity** — hooks can block a call but never rewrite its arguments, `PostToolUse` returns `{}` so output is immutable, and there is no base-URL override. Deny-only.
- **Kimi Code** — the hook API is allow/deny with no mutation anywhere. Its API traffic *is* proxyable, so the proxy layer is the route.
- **Roo Code** — has no hooks. Only the awareness file and an OpenAI-compatible base URL are available.

## Checking your own setup

```bash
rtk pipeline explain --agent cursor --tool Shell --command "git status"
```

`explain` reports the subject your tool maps to, which rules fire, and how far RTK can take the output on that agent. Agent names accept either spelling — `claude` and `claude-code` both work.

---

### Guide Resources Editions (`docs/guide/resources/editions.md`)

---
title: Editions and Licensing
description: What ships in each RTK download, which licence it carries, and how to install the Apache-2.0 edition
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](#licence-terms) 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](../proxy/overview.md). It is independent of both `rtk` editions.

## What the Apache-2.0 edition leaves out

It is the same binary minus the compression engine. What that removes:

| Absent | What you lose |
|---|---|
| `rtk pipeline` | Inspecting, validating, explaining and benchmarking pipelines |
| `rtk methods` | Listing registered compression methods |
| `rtk tasks` | Listing and running delegation tasks |
| PostToolUse compression | Compressing the output of an agent's own `Read` and MCP tools |
| Pipeline files | A `pipeline.toml` on disk is inert, exactly as in a build predating the engine |

**What stays is the part you use all day.** The PreToolUse rewrite hook — the mechanism that turns your agent's `git status` into `rtk git status` — is Apache-2.0 and behaves identically. So does every command filter, `rtk init`, `rtk gain`, `rtk discover`, `rtk session`, `rtk recall`, and `rtk llm-proxy`.

The practical difference: bash output is filtered exactly as in the full edition; output your agent reads through its *own* file and MCP tools is not.

## Installing the Apache-2.0 edition

Set `RTK_EDITION=oss` for the install script:

```bash
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | RTK_EDITION=oss sh
```

Homebrew ships it as a separate formula:

```bash
brew install rtk-ai/tap/rtk-oss
```

Debian and RPM packages are published as `rtk-oss`.

The two editions cannot coexist — both install `/usr/bin/rtk`. Homebrew declares this explicitly (`conflicts_with`), so `brew` refuses the second install with a clear message. The deb and rpm packages carry no declared `Conflicts:`, so `dpkg`/`rpm` stop on the file collision instead. **Uninstall one edition before installing the other.**

From a source checkout:

```bash
cargo build --release --no-default-features
```

`--no-default-features` removes `rtk-engine` from the dependency graph entirely — no BUSL-licensed code is compiled into the binary.

## Licence terms

- **Apache-2.0** covers the `rtk` CLI and its filters.
- **BUSL-1.1** covers the compression engine and the `rtk-proxy` daemon.

The BUSL Additional Use Grant permits **free production use**, including running the daemon. What it does not permit is offering RTK — or a derivative — to third parties as a commercial product or service.

For resale, embedding in a commercial offering, or any use the grant does not cover, contact **contact@rtk-ai.app**.

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
description: What RTK actually reduces, how bash output savings translate into cost, and why the token counts are estimates
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 result
```

The only thing RTK changes is **the bytes a shell command sends back**. Everything RTK reports as "savings" is measured on those bytes.

## The savings chain

```
Cost
├─ Input tokens
│  ├─ Bash output           <- the only part RTK filters
│  ├─ Your prompt
│  ├─ System prompt
│  └─ Conversation history
└─ Output tokens            <- what the model writes
```

Those bytes are **one contributor to input tokens**, alongside your prompt, the system prompt, and conversation history. Input tokens are in turn **only part of the bill**, which also counts output tokens.

So the reduction dilutes at every step: a large cut in bash output produces a smaller cut in input tokens, and a smaller one again in cost. A command showing 90% fewer output bytes does not make your session 90% cheaper.

This is why RTK reports bash output reduction rather than a cost figure. Bash output is the part RTK controls; the rest depends on your prompt, your model, how much the agent writes back, and how much of the conversation is replayed on each call.

## Why the token counts are estimates

`rtk gain` estimates tokens as `bytes / 4` (rounded up). RTK ships **no tokenizer**, so:

- **The percentage is reliable.** The same estimator is applied to the raw output and the filtered output, so the ratio between them holds regardless of the estimator's absolute accuracy.
- **The absolute token counts are approximate.** They will not match your provider's billing. Treat a figure such as `Input tokens: 50,000` as an order of magnitude, not an invoice line.

If you need exact counts, run the raw and filtered output through your model's own tokenizer.

## How to read `rtk gain`

| Column | What it actually is |
|--------|---------------------|
| Input | Estimated tokens from raw command output, `bytes / 4` |
| Output | Estimated tokens after filtering, `bytes / 4` |
| Saved | Input minus Output, in estimated tokens |
| Save% | Reduction in bash output bytes |

`Save%` is the meaningful number. It is a byte ratio, and it is accurate as a ratio.

## What RTK does not reduce

- **Output tokens.** RTK never touches what the model writes.
- **Your prompt, the system prompt, or conversation history.** These are input tokens RTK has no visibility into.
- **Commands with no matching filter.** These pass through untouched and are tracked at 0% savings. See `rtk gain --history`.

## See also

- [What RTK Optimizes](what-rtk-covers.md) — what each supported command filter changes
- [Token Savings Analytics](../analytics/gain.md) — reading the `rtk gain` dashboard

---

### Guide Resources Telemetry (`docs/guide/resources/telemetry.md`)

---
title: Telemetry & Privacy
description: What RTK collects, how to opt out, and your GDPR rights
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**: contact@rtk-ai.app

## How it works

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

## What is collected

Example values below are illustrative.

### Identity (anonymous)

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

### Environment

| Field | Example | Description |
|-------|---------|-------------|
| `version` | `1.0.0` | RTK version |
| `os` | `macos` | Operating system |
| `arch` | `aarch64` | CPU architecture |
| `install_method` | `homebrew` | How RTK was installed (homebrew/cargo/script/nix) |

### Usage volume

| Field | Example | Description |
|-------|---------|-------------|
| `commands_24h` | `100` | Commands run in the last 24 hours |
| `commands_total` | `10000` | Commands run since install |
| `top_commands` | `["git", "cargo", "ls"]` | Most used tools (names only, max 5) |
| `tokens_saved_24h` | `100000` | Estimated tokens saved in the last 24 hours |
| `tokens_saved_total` | `10000000` | Estimated tokens saved since install |
| `savings_pct` | `70.0` | Overall bash output reduction |

### Filter quality

| Field | Example | Description |
|-------|---------|-------------|
| `passthrough_top` | `["git:10", "npm:5"]` | Top 5 commands with 0% bash output reduction |
| `parse_failures_24h` | `0` | Filter parse failures in the last 24 hours |
| `low_savings_commands` | `["rtk <cmd>:25%"]` | Commands with a weighted bash output reduction below 30% (net-negative ones included). The rate is `SUM(saved)/SUM(input)` over every call of the command, the same figure as the `rtk gain` By Command table. Exact 0% is reported in `passthrough_top` instead |
| `avg_savings_per_command` | `70.0` | Unweighted average of per-command reduction rates across distinct command names; commands that never had any input are skipped |

### Ecosystem distribution

| Field | Example | Description |
|-------|---------|-------------|
| `ecosystem_mix` | `{"git": 50, "cargo": 20, "js": 10}` | Share of commands per tool category, in percent |

### Activity

| Field | Example | Description |
|-------|---------|-------------|
| `first_seen_days` | `30` | Installation age in days |
| `active_days_30d` | `20` | Days with at least 1 command in the last 30 days |

### Economics

| Field | Example | Description |
|-------|---------|-------------|
| `tokens_saved_30d` | `1000000` | Estimated tokens saved in the last 30 days |
| `estimated_savings_usd_30d` | — | A USD value derived from the estimated tokens saved and a fixed per-token rate. It is not a measured cost and does not reflect any provider's pricing |

### Integration

| Field | Example | Description |
|-------|---------|-------------|
| `hook_type` | `claude` | Which AI agent hook is installed (claude/gemini/codex/cursor/none) |
| `custom_toml_filters` | `1` | Number of user-created TOML filter files |

### Configuration

| Field | Example | Description |
|-------|---------|-------------|
| `has_config_toml` | `true` | Whether a `config.toml` exists |
| `exclude_commands_count` | `2` | Number of entries in `hooks.exclude_commands` |
| `projects_count` | `5` | Number of distinct project paths |

### Feature usage

| Field | Example | Description |
|-------|---------|-------------|
| `meta_usage` | `{"gain": 5, "discover": 2}` | How often RTK's own commands are run |

## What is NOT collected

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

## 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.

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

Environment variable override (blocks telemetry regardless of consent):
```bash
export RTK_TELEMETRY_DISABLED=1
```

## Retention Policy

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

## Your Rights (GDPR)

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

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

## Erasure Procedure

1. Run `rtk telemetry forget` — this disables telemetry, deletes your device salt, ping marker, and local tracking database (`history.db`), then sends an erasure request to the server.
2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email contact@rtk-ai.app for manual erasure.
3. You can also email contact@rtk-ai.app 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
description: Common RTK issues and how to fix them
sidebar:
  order: 3
---

# Troubleshooting

## `rtk gain` says "not a rtk command"

**Symptom:**
```bash
$ rtk gain
rtk: 'gain' is not a rtk command. See 'rtk --help'.
```

**Cause:** You installed **Rust Type Kit** (`reachingforthejack/rtk`) instead of **Rust Token Killer** (`rtk-ai/rtk`). They share the same binary name.

**Fix:**
```bash
cargo uninstall rtk
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh | sh
rtk gain    # should now show token savings stats
```

## How to tell which rtk you have

| If `rtk gain`... | You have |
|------------------|----------|
| Shows token savings dashboard | Rust Token Killer ✅ |
| Returns "not a rtk command" | Rust Type Kit ❌ |

## AI assistant not using RTK

**Symptom:** Claude Code (or another agent) runs `cargo test` instead of `rtk cargo test`.

**Checklist:**

1. Verify RTK is installed:
   ```bash
   rtk --version
   rtk gain
   ```

2. Initialize the hook:
   ```bash
   rtk init --global    # Claude Code
   rtk init --global --agent cursor    # Cursor
   rtk init --global --opencode  # OpenCode
   ```

3. Restart your AI assistant.

4. Verify hook status:
   ```bash
   rtk init --show
   ```

5. Check `settings.json` has the hook registered (Claude Code):
   ```bash
   cat ~/.claude/settings.json | grep rtk
   ```

## RTK not found after `cargo install`

**Symptom:**
```bash
$ rtk --version
zsh: command not found: rtk
```

**Cause:** `~/.cargo/bin` is not in your PATH.

**Fix:**

For bash (`~/.bashrc`) or zsh (`~/.zshrc`):
```bash
export PATH="$HOME/.cargo/bin:$PATH"
```

For fish (`~/.config/fish/config.fish`):
```fish
set -gx PATH $HOME/.cargo/bin $PATH
```

Then reload:
```bash
source ~/.zshrc    # or ~/.bashrc
rtk --version
```

## RTK on Windows

### Double-clicking rtk.exe does nothing

**Symptom:** You double-click `rtk.exe`, a terminal flashes and closes instantly.

**Cause:** RTK is a command-line tool. With no arguments, it prints usage and exits. The console window opens and closes before you can read anything.

**Fix:** Open a terminal first, then run RTK from there:
- Press `Win+R`, type `cmd`, press Enter
- Or open PowerShell or Windows Terminal
- Then run: `rtk --version`

### Hook not working (no auto-rewrite)

**Symptom:** On native Windows, commands are not auto-rewritten. An older `rtk init -g` printed "Falling back to --claude-md mode".

**Cause:** Before v0.37.2, `rtk init -g` registered no hook on native Windows: it fell back to injecting the full RTK instructions into `~/.claude/CLAUDE.md`. A setup made then still has no hook.

**Fix:** Upgrade to v0.37.2 or later, where `rtk init -g` registers the auto-rewrite hook on Windows as a native binary command (`rtk hook claude`). No Unix shell, bash, or jq is required. Re-run `rtk init -g`: it replaces the CLAUDE.md block with an `@RTK.md` reference and, once you confirm, adds `rtk hook claude` to `settings.json`. A legacy `~/.claude/hooks/rtk-rewrite.sh` hook, if one exists, is deleted along with its `.rtk-hook.sha256` and its `settings.json` entry.

Answer `y` when it asks to patch `settings.json`. Outside a terminal it cannot ask and defaults to `N`, so use `--auto-patch` there:

```powershell
rtk init -g                # answer y at the settings.json prompt
rtk init -g --auto-patch   # or: patch settings.json without asking
```

The default `N` leaves no hook registered: any legacy `rtk-rewrite.sh` entry is removed and `rtk hook claude` is not added. The `RTK hook registered (global).` banner prints either way; the line that confirms the patch is `settings.json: hook added` (or `settings.json: hook already present` when an earlier run added it). Restart Claude Code, then confirm:

```powershell
rtk init --show
```

It should report `[ok] Hook: rtk hook claude (native binary command)`.

[WSL](https://learn.microsoft.com/en-us/windows/wsl/install) also works and behaves like Linux if you prefer it.

### Node.js tools not found

**Symptom:**
```
rtk vitest --run
Error: program not found
```

**Cause:** On Windows, Node.js tools are installed as `.CMD`/`.BAT` wrappers. Older RTK versions couldn't find them.

**Fix:** Update to RTK v0.23.1+:
```bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
rtk --version    # should be 0.23.1+
```

## Compilation error during installation

```bash
rustup update stable
rustup default stable
cargo clean
cargo build --release
cargo install --path . --force
```

Minimum required Rust version: 1.91 (edition 2024 needs Cargo 1.85 or newer).

## OpenCode not using RTK

```bash
rtk init --global --opencode
# restart OpenCode
rtk init --show    # should show "OpenCode: plugin installed"
```

The plugin needs OpenCode 1.18.29 or newer. With an older version, `rtk init`
installs nothing and prints the version it found, when it can read
`opencode --version`. Upgrade OpenCode rather than looking for a rewrite
setting.

If the plugin loads but nothing changes, check that `rtk` is on the `PATH`
OpenCode itself sees. The plugin only resolves it from `PATH`, because
OpenCode's shell tool does not source `.profile`, `.bashrc` or `.bash_profile`
and a binary found elsewhere cannot be spawned as a bare `rtk`. Set
`RTK_DISABLED=1` to confirm which way round the behaviour goes.

## `cargo install rtk` installs the wrong package

If Rust Type Kit is published to crates.io under the name `rtk`, `cargo install rtk` may install the wrong one.

Always use the explicit URL, pinned to the release branch:

```bash
cargo install --git https://github.com/rtk-ai/rtk --branch master rtk
```

## Does RTK break Claude's prompt cache?

No. RTK filters command output once, at execution time. The filtered result is written into the
conversation history and never changes afterwards, and prompt caching matches on a stable prefix
— RTK does not rewrite anything the cache has already seen.

Smaller tool results also make caching cheaper: cache writes bill at 1.25x and cache reads at
0.1x the input rate, so fewer tokens in means less to write once and less to re-read every turn.

To see your own cache write and read volumes next to RTK's savings:

```bash
rtk cc-economics
```

`rtk gain` reports token savings only; the cache breakdown lives in `rtk cc-economics`.

## Run the diagnostic script

From the RTK repository root:

```bash
bash scripts/check-installation.sh
```

Checks:
- RTK installed and in PATH
- Correct version (Token Killer, not Type Kit)
- Available features
- Claude Code integration
- Hook status

## Still stuck?

Open an issue: https://github.com/rtk-ai/rtk/issues

---

### Guide Resources What Rtk Covers (`docs/guide/resources/what-rtk-covers.md`)

---
title: What RTK Optimizes
description: Commands and ecosystems automatically optimized by RTK
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](./savings-explained.md).
:::

## 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) |

```bash
# Debug: see what RTK is doing
rtk -vvv git status
```

## Commands that are not rewritten

If a command isn't in the list above, RTK runs it through passthrough — the output reaches the LLM unchanged. You can explicitly track unsupported commands:

```bash
rtk bypass make install    # runs make install, tracks usage, no filtering
```

To check which commands were missed opportunities: `rtk discover`.

---

--- METRICS ---
- Files Extracted: 71
- Estimated Token Budget: ~119157 tokens
- Canonical Reference: https://codewiki.google/github.com/rtk-ai/rtk
