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:
lookup_command_for_filter() in src/core/toml_filter.rs
- 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 all → rtk rewrite → rtk 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:
- Create filter in
src/filters/
- Add rewrite pattern in
src/discover/rules.rs
- 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/grep → grep) 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 all → make 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 |
Summary
TOML built-in filters (e.g.
make.toml, futurebru.toml,ng-test.toml) work when the user runsrtk <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_fallbackinmain.rsalready normalizesargs[0]to basename for TOML lookup —rtk rewrite/discover/registry.rsdoes not. Hooks return no rewrite → agent runs raw command → no token savings.Proposed fix: shared infrastructure for TOML path rewrite:
lookup_command_for_filter()insrc/core/toml_filter.rsrewrite_segment_inner()insrc/discover/registry.rsScope: 2 Rust files, ~48 lines, tests on existing
makefilter (no new.tomlfilters 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
rtk make alllookup_cmd = "make all"(basename inrun_fallback)make all→rtk rewrite→rtk make allrules.rsprefixmakenode_modules/.bin/make allrules.rsmatch for pathnode_modules/.bin/bru run …rules.rsmatch; TOML exists but hook never prependsrtkCONTRIBUTING checklist assumes
rules.rsonlyFrom
src/cmds/README.md— TOML filter checklist:src/filters/src/discover/rules.rsFor tools invoked only via
.bin/paths or with wrappers (npx bru,yarn ng test), adding arules.rsentry per variant does not scale and still misses compound/path edge cases.Root cause
run_fallbackalready has basename logicSo
rtk node_modules/.bin/make allexecutes and filters correctly once thertkprefix is present.registry.rsrewrite has no TOML fallbackrewrite_segment_inner()only rewrites viarules.rsprefix matching:strip_absolute_path()exists for classification (/usr/bin/grep→grep) but is not used to detect TOML-only tools without a dedicated rule.Cause → effect chain
Same for
ng testvianode_modules/.bin/ng,yarn ng test(needsyarnrule separately), etc.Proposed solution
1.
lookup_command_for_filter(command: &str) -> StringExtract basename normalization (same semantics as
run_fallback):Reusable for rewrite lookup and tests.
2. TOML fallback in
rewrite_segment_inner()After
rules.rsprefix loop, beforeNone:Design choice: prepend
rtkto the original command (keep real binary path inargs[0]) sorun_fallbackexecutes the same binary the user/agent intended.3. Tests (existing
makefilter — no new filters in this PR)test_lookup_command_for_filter_uses_basename/usr/local/bin/make all→make alltest_builtin_make_filter_matches_path_invocationmakebuilt-in filtertest_rewrite_toml_filter_bin_pathrtk rewrite '/usr/local/bin/make all'→rtk /usr/local/bin/make allUsing
makekeeps this PR filter-agnostic — bru/ng PRs only add.toml+rules.rs, not duplicate infrastructure.Reproduction (before fix)
With Cursor hook (after cursor exit-3 fix):
What this PR does not include
src/filters/bru.tomlfeat/bru-toml-filtersrc/filters/ng-test.tomlfeat/ng-test-karma-toml-filterhooks/cursor/rtk-rewrite.shexit 3 fixfeat/cursor-hook-exit-3rules.rsentries forbru,ng test,yarn ng testThis PR is shared plumbing only.
Edge cases considered
rules.rsfirstrtk <original-path-cmd>None(unchanged passthrough)RTK_DISABLED=1cd … && node_modules/.bin/makefind_matching_filtermatches — same guard asrun_fallback