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

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, adding a rewrite rule in 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.

/// 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.

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)

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.

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

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, following the pattern of the Git rule at lines 14-20【src/discover/rules.rs#L14-L20】:

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.

#[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 if the filter introduces new patterns
  • Contributing guide: Reference the new command in 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 by extending the Commands enum and adding a match arm in run_cli().
  • Define a rewrite rule in 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 to maintain type safety and testability.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →