# How to Add a New Command Filter to RTK: A 5-Step Implementation Guide

> Learn to add a new command filter to RTK with this 5-step guide. Implement Rust modules, register commands, and write tests for efficiency.

- Repository: [rtk-ai/rtk](https://github.com/rtk-ai/rtk)
- Tags: how-to-guide
- Published: 2026-04-24

---

**Adding a new command filter to RTK requires creating a Rust module in `src/cmds/` that implements a pure filter function and `run()` entry point, registering the command in [`src/main.rs`](https://github.com/rtk-ai/rtk/blob/main/src/main.rs), adding a rewrite rule in [`src/discover/rules.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/rules.rs), and writing tests that verify ≥60% token savings.**

RTK (Repository Tool Kit) separates *command discovery* (rewrite rules that transform raw CLI invocations) from *command execution* (Rust modules that run external tools and filter output). This architecture ensures that every new command filter you add to the `rtk-ai/rtk` repository is automatically tracked for token savings, supports hook-based rewriting, and maintains consistent error handling.

## Step 1: Create the Filter Module in `src/cmds/`

New filters live in ecosystem-specific subdirectories under `src/cmds/`. For example, Git commands reside in `src/cmds/git/`, while custom tools use `src/cmds/custom/`. The module must expose a `run()` function that returns `Result<i32>` and follows the skeleton outlined in the command-module README at lines 68-89【src/cmds/README.md#L68-L89】.

### The Pure Filter Function

The core logic is a pure function with the signature `&str → String` that transforms raw tool output into compact, LLM-optimized text. This function contains no side effects and operates only on the stdout/stderr strings passed to it.

```rust
/// Pure filter – turn raw stdout into compact output
fn filter_mycmd_output(raw: &str) -> String {
    // … implement your transformation here …
    raw.lines()
        .filter(|l| !l.contains("unimportant"))
        .collect::<Vec<_>>()
        .join("\n")
}

```

### The Run Entry Point

The `run()` function builds the external `Command`, configures `RunOptions`, and delegates to one of the runner helpers (`run_filtered`, `run_streamed`, or `run_passthrough`). Using these helpers guarantees automatic exit-code propagation, token-savings tracking via `core::tracking`, and optional tee-recovery on failure.

```rust
use crate::core::runner;
use crate::core::utils::resolved_command;

/// Entry point called from `main.rs`
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(" "));
    }

    // Most filters only need stdout, so we use `stdout_only()` and enable tee recovery
    runner::run_filtered(
        cmd,
        "mycmd",
        &args.join(" "),
        filter_mycmd_output,
        runner::RunOptions::stdout_only().tee("mycmd"),
    )
}

```

## Step 2: Register the Command in the CLI ([`src/main.rs`](https://github.com/rtk-ai/rtk/blob/main/src/main.rs))

To make the filter discoverable by users, add a variant to the `Commands` enum around lines 73-79【src/main.rs#L73-L79】 and a corresponding match arm in `run_cli()` that forwards arguments to your module.

```rust
#[derive(Debug, Subcommand)]
enum Commands {
    // … existing variants …
    /// My custom command with token‑optimized output
    Mycmd {
        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
        args: Vec<String>,
    },
    // …
}

// In the run_cli match:
Commands::Mycmd { args } => mycmd_cmd::run(&args, cli.verbose)?,

```

This registration exposes `rtk mycmd [ARGS]` to the command line and routes parsed arguments through Clap directly to your filter implementation.

## Step 3: Add the Rewrite Rule ([`src/discover/rules.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/rules.rs))

The **rewrite rule** enables RTK's hook system to automatically transform raw invocations (e.g., `git log …`) into filtered RTK calls (`rtk git log …`). Without this entry, the auto-rewrite engine treats the command as unknown and falls back to raw execution.

Insert a new `RtkRule` struct literal into the `RULES` array in [`src/discover/rules.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/rules.rs), following the pattern of the Git rule at lines 14-20【src/discover/rules.rs#L14-L20】:

```rust
RtkRule {
    // Matches “mycmd <any‑args>”
    pattern: r"^mycmd\s+",
    rtk_cmd: "rtk mycmd",
    rewrite_prefixes: &["mycmd"],
    category: "Custom",
    savings_pct: 70.0,
    subcmd_savings: &[],
    subcmd_status: &[],
},

```

The `pattern` field uses regex to identify the raw command, while `rtk_cmd` specifies the RTK equivalent that the hook will substitute.

## Step 4: Write Tests for Token Savings

Every filter must include tests that verify both functional correctness and the project's token-saving guarantees (≥60% reduction). Create test files in `src/cmds/<ecosystem>/tests/` following the pattern used by existing modules like [`src/cmds/git/git_log_test.rs`](https://github.com/rtk-ai/rtk/blob/main/src/cmds/git/git_log_test.rs).

```rust
#[test]
fn test_mycmd_filter() {
    // Simulate raw tool output
    let raw = "info: start\nunimportant line\nerror: failure\n";
    let filtered = mycmd_cmd::filter_mycmd_output(raw);
    assert_eq!(filtered, "info: start\nerror: failure");
    // Ensure we save at least 60 % tokens (snapshot test helper)
    crate::test_helpers::assert_token_savings(&filtered, raw, 60.0);
}

```

Tests should cover all four `FilterMode` variants (`CaptureOnly`, `Buffered`, `Streaming`, `Passthrough`) if your filter supports multiple execution strategies.

## Step 5: Update Documentation

Complete the implementation by updating user-facing documentation:

- **Ecosystem README**: Add usage examples to `src/cmds/<ecosystem>/README.md`
- **Architecture docs**: Update [`ARCHITECTURE.md`](https://github.com/rtk-ai/rtk/blob/main/ARCHITECTURE.md) if the filter introduces new patterns
- **Contributing guide**: Reference the new command in [`CONTRIBUTING.md`](https://github.com/rtk-ai/rtk/blob/main/CONTRIBUTING.md) if it affects developer workflows
- **Changelog**: The project uses release-please for auto-generated changelogs; ensure your commit messages follow the Conventional Commits specification

## Summary

- **Create a Rust module** in `src/cmds/<ecosystem>/` containing a pure filter function and a `run()` entry point that uses `runner::run_filtered()` for consistent execution.
- **Register the CLI variant** in [`src/main.rs`](https://github.com/rtk-ai/rtk/blob/main/src/main.rs) by extending the `Commands` enum and adding a match arm in `run_cli()`.
- **Define a rewrite rule** in [`src/discover/rules.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/rules.rs) so the hook system can automatically rewrite raw commands to RTK equivalents.
- **Write comprehensive tests** that assert both output correctness and ≥60% token savings using the project's test harness.
- **Update documentation** across ecosystem READMEs, architecture docs, and changelogs to maintain discoverability.

## Frequently Asked Questions

### What is the difference between `run_filtered`, `run_streamed`, and `run_passthrough`?

**`run_filtered`** buffers stdout, applies your pure filter function, and prints the compacted result—ideal for commands where you need to parse and reduce output. **`run_streamed`** processes output line-by-line in real time without buffering the entire stream, suitable for long-running processes where latency matters. **`run_passthrough`** executes the command without transformation, used when you only need RTK's exit-code handling and tracking but no filtering logic.

### Why does the rewrite rule require both a regex pattern and an `rtk_cmd` string?

The **regex pattern** identifies the raw command invocation in the user's shell history or script, while the **`rtk_cmd`** string specifies the exact replacement text that the hook injects. This separation allows complex patterns (e.g., matching `git log` with various flags) to map to a single, canonical RTK command structure, ensuring consistent behavior regardless of how the user originally typed the command.

### How does RTK enforce the 60% token savings requirement?

The test harness in `crate::test_helpers` calculates the byte difference between raw tool output and filtered output, asserting that the reduction meets or exceeds 60%. This threshold ensures that every filter in the RTK ecosystem provides meaningful value for LLM context windows. Filters that fail this assertion during `cargo test` must be optimized to remove more redundant or non-semantic content.

### Can I implement a filter using TOML configuration instead of Rust?

Yes, for simple filters that only need line-based inclusion or exclusion rules, you can define a **TOML filter** rather than a full Rust module. However, complex transformations requiring parsing, aggregation, or semantic analysis must use the Rust module approach described in [`src/cmds/README.md`](https://github.com/rtk-ai/rtk/blob/main/src/cmds/README.md) to maintain type safety and testability.