Skip to content

feat(rewrite): auto-rewrite TOML-only commands invoked via binary paths (.bin/, absolute paths) #2721

Description

@denis-peshkov

Summary

TOML built-in filters (e.g. make.toml, future bru.toml, ng-test.toml) work when the user runs rtk <tool> … directly, but Cursor/Claude hooks do not rewrite equivalent invocations via:

  • node_modules/.bin/<tool> …
  • /usr/local/bin/<tool> …
  • ../../client/node_modules/.bin/bru run …

run_fallback in main.rs already normalizes args[0] to basename for TOML lookup — rtk rewrite / discover/registry.rs does not. Hooks return no rewrite → agent runs raw command → no token savings.

Proposed fix: shared infrastructure for TOML path rewrite:

  1. lookup_command_for_filter() in src/core/toml_filter.rs
  2. TOML-match fallback in rewrite_segment_inner() in src/discover/registry.rs

Scope: 2 Rust files, ~48 lines, tests on existing make filter (no new .toml filters in this PR).

Depends on: nothing (can merge after or in parallel with fix(cursor): exit code 3 — they are independent).

Blocks: TOML filter PRs that rely on path/wrapper invocations (bru, ng test, etc.).


Problem statement

Two execution paths, one gap

Path Entry TOML lookup Works today?
Direct rtk make all lookup_cmd = "make all" (basename in run_fallback)
Hook Agent: make allrtk rewritertk make all rules.rs prefix make
Hook + path Agent: node_modules/.bin/make all No rules.rs match for path
Hook + path Agent: node_modules/.bin/bru run … No rules.rs match; TOML exists but hook never prepends rtk

CONTRIBUTING checklist assumes rules.rs only

From src/cmds/README.md — TOML filter checklist:

  1. Create filter in src/filters/
  2. Add rewrite pattern in src/discover/rules.rs
  3. Write tests

For tools invoked only via .bin/ paths or with wrappers (npx bru, yarn ng test), adding a rules.rs entry per variant does not scale and still misses compound/path edge cases.


Root cause

run_fallback already has basename logic

// main.rs — TOML lookup for `rtk <cmd> …`
let base = Path::new(&args[0]).file_name();
let lookup_cmd = [base, …args[1..]].join(" ");
core::toml_filter::find_matching_filter(&lookup_cmd)

So rtk node_modules/.bin/make all executes and filters correctly once the rtk prefix is present.

registry.rs rewrite has no TOML fallback

rewrite_segment_inner() only rewrites via rules.rs prefix matching:

for &prefix in rule.rewrite_prefixes {
    if let Some(rest) = strip_word_prefix(cmd_part, prefix) {}
}
None  // ← path invocations fall through

strip_absolute_path() exists for classification (/usr/bin/grepgrep) but is not used to detect TOML-only tools without a dedicated rule.

Cause → effect chain

Contributor adds src/filters/bru.toml (match_command = "\\bbru\\b")
        ↓
User/agent runs: node_modules/.bin/bru run Identity/Token
        ↓
Cursor hook: rtk rewrite "node_modules/.bin/bru run …"
        ↓
registry.rs: no rules.rs prefix matches "node_modules/.bin/bru"
        ↓
rtk rewrite → exit 1, no output
        ↓
Hook returns {} (no rewrite)
        ↓
Agent runs raw bru → verbose output, no RTK savings
        ↓
Even manual `rtk node_modules/.bin/bru run …` would work IF hook had rewritten it

Same for ng test via node_modules/.bin/ng, yarn ng test (needs yarn rule separately), etc.


Proposed solution

1. lookup_command_for_filter(command: &str) -> String

Extract basename normalization (same semantics as run_fallback):

/usr/local/bin/make all           → make all
node_modules/.bin/ng test auth    → ng test auth
../../client/node_modules/.bin/bru run Identity → bru run Identity

Reusable for rewrite lookup and tests.

2. TOML fallback in rewrite_segment_inner()

After rules.rs prefix loop, before None:

let lookup = toml_filter::lookup_command_for_filter(cmd_part);
if toml_filter::find_matching_filter(&lookup).is_some()
    || toml_filter::find_matching_filter(cmd_part).is_some()
{
    return Some(format!("rtk {}{}", cmd_part, redirect_suffix));
}

Design choice: prepend rtk to the original command (keep real binary path in args[0]) so run_fallback executes the same binary the user/agent intended.

3. Tests (existing make filter — no new filters in this PR)

Test Asserts
test_lookup_command_for_filter_uses_basename /usr/local/bin/make allmake all
test_builtin_make_filter_matches_path_invocation lookup matches make built-in filter
test_rewrite_toml_filter_bin_path rtk rewrite '/usr/local/bin/make all'rtk /usr/local/bin/make all

Using make keeps this PR filter-agnostic — bru/ng PRs only add .toml + rules.rs, not duplicate infrastructure.


Reproduction (before fix)

# TOML filter works when rtk prefix is present
rtk /usr/local/bin/make all   # or: rtk make all
# → filtered output

# Hook rewrite does NOT add rtk prefix for path invocation
rtk rewrite '/usr/local/bin/make all'
# exit 1, empty stdout (before fix)

# After fix:
rtk rewrite '/usr/local/bin/make all'
# stdout: rtk /usr/local/bin/make all

With Cursor hook (after cursor exit-3 fix):

echo '{"tool_input":{"command":"node_modules/.bin/make all"}}' | hooks/cursor/rtk-rewrite.sh
# Before: {}
# After this PR (+ cursor hook fix): {"updated_input":{"command":"rtk node_modules/.bin/make all"}}

What this PR does not include

Item Separate PR
src/filters/bru.toml feat/bru-toml-filter
src/filters/ng-test.toml feat/ng-test-karma-toml-filter
hooks/cursor/rtk-rewrite.sh exit 3 fix feat/cursor-hook-exit-3
rules.rs entries for bru, ng test, yarn ng test bru / ng-test PRs

This PR is shared plumbing only.


Edge cases considered

Case Behavior
Command matches rules.rs first Prefix rewrite wins (unchanged)
Path + TOML match rtk <original-path-cmd>
No TOML match None (unchanged passthrough)
RTK_DISABLED=1 Skipped earlier in rewrite chain (unchanged)
Compound cd … && node_modules/.bin/make Per-segment rewrite (unchanged compound logic)
False positive? Only if find_matching_filter matches — same guard as run_fallback

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions