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.mdif the filter introduces new patterns - Contributing guide: Reference the new command in
CONTRIBUTING.mdif 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 arun()entry point that usesrunner::run_filtered()for consistent execution. - Register the CLI variant in
src/main.rsby extending theCommandsenum and adding a match arm inrun_cli(). - Define a rewrite rule in
src/discover/rules.rsso 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →