rtk-ai / rtk-ai/rtk

Design: TOML filters cannot reach commands routed by Rust modules — intercept default passthrough

Open
#3,904 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
81.1k
Forks
5.1k
Avg merge
4d 21h
Merged PRs (30d)
35

Description

Problem

A built-in TOML filter can be permanently unreachable, and nothing detects it.

src/filters/gradle.toml is the worked example. It has never filtered a single invocation since it was added:

  • run_fallback matches against the basename of args[0] (src/main.rs:1483-1487), so the \./ branch of its match_command can never fire.
  • That basename logic landed in adda2537 (2026-03-10); gradle.toml was added in 8fae5b06 (2026-03-18) — the file was born unreachable.
  • Independently, src/discover/rules.rs:785 rewrites gradle, gradlew, ./gradlew and gradlew.bat to rtk gradlew, so Clap routes them to gradlew_cmd before run_fallback is ever reached.

Verified against a develop build:

$ rtk rewrite "gradle build"     → rtk gradlew build
$ rtk rewrite "gradle bootRun"   → rtk gradlew bootRun
$ rtk rewrite "./gradlew build"  → rtk gradlew build

The filter is dead from unreachability, not redundancygradlew_cmd does not do its job. It classifies build/test/lint/dependencies/connected and drops everything else into GradlewTask::Otherrun_passthrough, fully unfiltered:

rtk gradlew bootRun    164 lines in → 164 out     (Other → passthrough)
rtk gradlew publish    164 lines in → 164 out
rtk gradlew tasks      164 lines in → 164 out
rtk gradlew build      164 lines in →   2 out     (Build → filtered)

So the noise gradle.toml was written to strip is exactly the noise gradlew_cmd declines to touch — and the two can never meet. This is structural, not gradle-specific: any Rust module that routes a command family shadows every TOML filter for that family, silently. Two more built-in filters are already dead the same way — confirmed below.

The guard meant to catch this doesn't: compile_filter's shadow warning only checks RUST_HANDLED_COMMANDS (src/core/toml_filter.rs:259), which omits gradlew among others, so no warning is emitted.

The seam already exists

FilterMode::Buffered is implemented and unused in production (src/core/stream.rs:244, #[allow(dead_code)]):

pub enum FilterMode<'a> {
    Streaming(Box<dyn StreamFilter + 'a>),
    #[allow(dead_code)]
    Buffered(Box<dyn Fn(&str) -> String + 'a>),
    CaptureOnly,
    Passthrough,
}

Its implementation (stream.rs:476-500) already has the 10 MiB RAW_CAP guard and wraps the filter in catch_unwind with raw-output fallback — the project's mandatory fallback pattern. And toml_filter::apply_filter(&CompiledFilter, &str) -> String matches Box<dyn Fn(&str) -> String> exactly.

Proposal

1. Split deliberate from default passthrough

run_passthrough currently serves two unrelated intents. In gradlew_cmd::run alone:

  • Deliberate--stacktrace, --info, --debug, --full-stacktrace, and rtk proxy: the user opted out of filtering. A TOML filter firing here would defeat an explicit request.
  • DefaultGradlewTask::Other: nobody wrote a filter yet.

Introduce run_passthrough_filterable() for the default sites, consulting the TOML registry; leave run_passthrough as the honest opt-out. Blast radius if applied bluntly: 75 call sites across 17 modules, dominated by gh_cmd (18) and glab_cmd (15), both of which pass through for interactive subcommands — so this must be opt-in per call site, not global.

2. Prefer Streaming over Buffered, and split the DSL accordingly

FilterMode::Passthrough early-returns with Stdio::inherit() and discards output entirely (stream.rs:305-311). Buffered waits for process exit — for a process that never exits, the user sees nothing, ever.

FilterMode::Streaming has neither problem. The DSL splits cleanly:

streamable needs the whole blob
strip_lines_matching, keep_lines_matching, replace, truncate_lines_at, head_lines, max_lines, on_empty tail_lines, match_output

gradle.toml uses only the left column, so it can stream. Use Streaming whenever a filter is streamable; reserve Buffered for the right column.

3. Generalize is_watch_mode — as a guard on the buffered path

runner.rs:454 already states the rule:

Watch mode never exits, and the filtered runners buffer the whole stream until the child does, so a watched run prints nothing at all and loses the buffer on Ctrl-C. Callers send these through unfiltered instead.

But is_watch_mode only detects --watch/--watch=, and only deno_cmd and bun_cmd call it. It doesn't know bootRun, --continuous/-t, -f/--follow, next dev, tail -f. gradlew_cmd never calls it — bootRun survives today only because passthrough happens to stream.

Generalize to is_long_running(tool, args), but apply it as a guard on the buffered path, not as a blanket pre-filter. Rationale: "long-running" is a property of the run, not the command line. A gradle bootRun that crashes in 3s is short-lived, and that is exactly the case spring-boot.toml serves well today (164 raw lines → 240 bytes with the error intact). Gating blindly on the task name would regress it; using Streaming where possible avoids needing to classify at all.

4. Close the detection gap
  • Fill RUST_HANDLED_COMMANDS so the existing shadow warning actually fires.
  • Add a test asserting which filter a given command line selects. Nothing does this today: run_filter_tests only executes the [[tests.*]] pipeline cases and never evaluates match_command, so a widened pattern can silently capture another filter's traffic. Note that ordering is by filter name via BTreeMap<String, TomlFilterDef> (toml_filter.rs:76), not by build.rs's filename sort, and the tiebreak is alphabetical rather than most-specific — so a broad ^gradle\b will always beat a narrower ^gradle\s+.*bootRun regardless of filenames.

Confirmed: two more built-in filters are already dead for the same reason

Swept all 63 built-in filters against the Clap subcommand list, then verified the two hits end to end against a develop build. Both have a distinctive match_output short-circuit, which makes them self-identifying: if the filter fires, its message is the entire output.

src/filters/dotnet-build.toml (^dotnet\s+build\b) — DotnetCommands::Build is an explicit Clap subcommand (main.rs:1273), so rtk dotnet build is routed to dotnet_cmd and run_fallback is never reached:

$ rtk dotnet build          # stdout: a clean build, "0 Warning(s) / 0 Error(s)"
fail dotnet build: 1 projects, 0 errors, 0 warnings (00:00:02.34)

The filter would have printed ok (build succeeded). It never runs.

src/filters/uv-sync.toml (^uv\s+(sync|pip\s+install)\b) — Commands::Uv takes trailing_var_arg (main.rs:828), so rtk uv sync is routed to uv_cmd:

$ rtk uv sync
Resolved 42 packages in 123ms
Audited 42 packages in 0.05ms

The filter would have printed ok (up to date). Instead the output passes through raw — zero savings on a case a working filter would have collapsed to four words. Same for uv pip install.

Both are also rewritten to the Clap route by the hook, so there is no path that reaches them:

rtk rewrite "dotnet build"           → rtk dotnet build
rtk rewrite "uv sync"                → rtk uv sync
rtk rewrite "uv pip install requests" → rtk uv pip install requests
Tally

Three of 63 built-in filters are fully unreachable — gradle.toml, dotnet-build.toml, uv-sync.toml — and none of them ever emitted a warning.

src/filters/spring-boot.toml is partially dead as well: its mvn\s+spring-boot:run branch cannot fire because mvn is a Clap subcommand, and its gradle\s+.*bootRun branch cannot fire on the hook path because the discover rule rewrites gradle … to rtk gradlew. Only the java -jar …spring….jar branch is live. The mvn half is deliberate and documented in the filter's own description; the gradle half is not.

Note the pattern in the two new cases: the Rust module owns the command family but declines to filter that particular subcommand (uv sync passes through raw), while the TOML filter that would have handled it is unreachable. This is the same shape as gradlew_cmd's GradlewTask::Other, and it is the clearest argument for interception at the default-passthrough sites rather than for deleting the filters.

Relationship to #2708 / #2709

#2708 diagnoses the gradle.toml regex as miswritten and #2709 widens it. The regex reading is correct, but the stated impact is not: ./gradle/./gradlew never worked either (basename), and gradle commands are filtered today, by gradlew_cmd. Widening the regex also has side effects — see the review on #2709.

The regex is the wrong lever. Under this proposal gradle.toml becomes reachable and useful without being widened at all; without it, the file should simply be deleted, since it has never done anything.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/main.rs routing, src/discover/rules.rs, src/core/stream.rs, src/core/toml_filter.rs, and the relevant command modules such as gradlew_cmd. Trace default versus deliberate passthrough and the existing filter tests before choosing interception points. Done means unreachable built-in filters are detected, eligible default passthrough can apply them without breaking explicit passthrough or long-running commands, and command-line filter selection is tested.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
cli, testing, tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.