Environment: rtk 0.37.2 · Ubuntu 24.04 (WSL2) · Claude Code hook (rtk hook claude)
Confirmed present on master (5d32d07) by reading src/discover/registry.rs — see Cause.
Summary
[hooks] exclude_commands is matched against the raw command text, but classify_command resolves wrapper/module forms to a different tool. When the two disagree, the exclusion silently fails and the command is rewritten anyway.
The clearest case is python -m <tool>: excluding "pytest" does not exclude python3 -m pytest, even though rtk classifies that command as rtk pytest.
Reproduction
# ~/.config/rtk/config.toml
[hooks]
exclude_commands = ["pytest"]
$ rtk rewrite "pytest tests/ -q"
$ echo $? # 1 — correctly excluded ✓
$ rtk rewrite "python3 -m pytest tests/ -q"
rtk pytest tests/ -q # rewritten despite the exclusion ✗
Not pytest-specific — same for any module-form tool:
# exclude_commands = ["mypy"]
$ rtk rewrite "mypy --strict ."
$ echo $? # 1 — excluded ✓
$ rtk rewrite "python3 -m mypy --strict ."
rtk mypy --strict . # rewritten despite the exclusion ✗
Cause
rewrite_segment_inner (src/discover/registry.rs, Classification::Supported arm, ~L884-891):
let rtk_equivalent = match classify_command(cmd_part) {
Classification::Supported { rtk_equivalent, .. } => {
let stripped = ENV_PREFIX.replace(cmd_part, "");
let cmd_clean = stripped.trim();
if is_excluded(cmd_clean, excluded) { // <-- raw text, not rtk_equivalent
return None;
}
rtk_equivalent
}
is_excluded prefix-matches (~L798):
ExcludePattern::Prefix(prefix) => cmd.starts_with(prefix.as_str()),
So for python3 -m pytest tests/:
classify_command → Classification::Supported { rtk_equivalent: "rtk pytest", .. } (asserted by the existing test test_classify_python_m_pytest, L2477)
is_excluded("python3 -m pytest tests/", ["pytest"]) → "python3 -m pytest tests/".starts_with("pytest") → false
- → not excluded → rewritten to
rtk pytest tests/
The exclusion check never sees the tool the classifier actually resolved. Note the existing rewrite tests for these forms (test_rewrite_python_m_pytest L2534, test_python3_m_pytest L4260) all pass &[] as the excluded list, so this path is untested with a non-empty exclusion.
Why it bites in practice
python -m pytest and bare pytest are not interchangeable: -m runs the current interpreter and puts CWD on sys.path; bare pytest runs whichever pytest shim is first on PATH. On this machine they are different interpreters (3.10 vs 3.11).
So the rewrite silently swapped the interpreter, imports failed, and pytest exited 2 (collection error) — which the pytest summarizer reported as Pytest: No tests collected (#3032), hiding the ModuleNotFoundError:
$ python3 -m pytest tests/ -q # hook rewrites this to `rtk pytest tests/ -q`
Pytest: No tests collected
$ rtk proxy python3 -m pytest tests/ -q
41 passed
The exclusion existed precisely to prevent this, and was bypassed. Two agents in one session read the filtered text and concluded working code was broken. ([tee] mode = "failures" did preserve the real traceback — that escape hatch worked.)
Suggested fix
Also test the resolved target, e.g. in the Supported arm:
Classification::Supported { rtk_equivalent, .. } => {
let stripped = ENV_PREFIX.replace(cmd_part, "");
let cmd_clean = stripped.trim();
// exclusion should apply to the tool rtk actually resolved to, not just the raw text
let target = rtk_equivalent.strip_prefix("rtk ").unwrap_or(rtk_equivalent);
if is_excluded(cmd_clean, excluded) || is_excluded(target, excluded) {
return None;
}
rtk_equivalent
}
That makes exclude_commands = ["pytest"] cover every form rtk maps to rtk pytest (python -m pytest, python3 -m pytest, …) without needing users to enumerate wrappers.
A regression test would be rewrite_command_no_prefixes("python3 -m pytest tests/", &[ExcludePattern::Prefix("pytest".into())]) → None.
Workaround
Exclude the interpreter as well as the tool:
exclude_commands = ["pytest", "python3", "python"]
This costs nothing today, since bare python3 script.py has no rewrite rule anyway (#2312, #928, #285 are open FRs to add one) — the only effect of the python -m path is to route to the wrapped tool's filter.
Related
Environment: rtk 0.37.2 · Ubuntu 24.04 (WSL2) · Claude Code hook (
rtk hook claude)Confirmed present on master (
5d32d07) by readingsrc/discover/registry.rs— see Cause.Summary
[hooks] exclude_commandsis matched against the raw command text, butclassify_commandresolves wrapper/module forms to a different tool. When the two disagree, the exclusion silently fails and the command is rewritten anyway.The clearest case is
python -m <tool>: excluding"pytest"does not excludepython3 -m pytest, even though rtk classifies that command asrtk pytest.Reproduction
Not pytest-specific — same for any module-form tool:
Cause
rewrite_segment_inner(src/discover/registry.rs,Classification::Supportedarm, ~L884-891):is_excludedprefix-matches (~L798):So for
python3 -m pytest tests/:classify_command→Classification::Supported { rtk_equivalent: "rtk pytest", .. }(asserted by the existing testtest_classify_python_m_pytest, L2477)is_excluded("python3 -m pytest tests/", ["pytest"])→"python3 -m pytest tests/".starts_with("pytest")→ falsertk pytest tests/The exclusion check never sees the tool the classifier actually resolved. Note the existing rewrite tests for these forms (
test_rewrite_python_m_pytestL2534,test_python3_m_pytestL4260) all pass&[]as the excluded list, so this path is untested with a non-empty exclusion.Why it bites in practice
python -m pytestand barepytestare not interchangeable:-mruns the current interpreter and puts CWD onsys.path; barepytestruns whicheverpytestshim is first on PATH. On this machine they are different interpreters (3.10 vs 3.11).So the rewrite silently swapped the interpreter, imports failed, and pytest exited 2 (collection error) — which the pytest summarizer reported as
Pytest: No tests collected(#3032), hiding theModuleNotFoundError:The exclusion existed precisely to prevent this, and was bypassed. Two agents in one session read the filtered text and concluded working code was broken. (
[tee] mode = "failures"did preserve the real traceback — that escape hatch worked.)Suggested fix
Also test the resolved target, e.g. in the
Supportedarm:That makes
exclude_commands = ["pytest"]cover every form rtk maps tortk pytest(python -m pytest,python3 -m pytest, …) without needing users to enumerate wrappers.A regression test would be
rewrite_command_no_prefixes("python3 -m pytest tests/", &[ExcludePattern::Prefix("pytest".into())])→None.Workaround
Exclude the interpreter as well as the tool:
This costs nothing today, since bare
python3 script.pyhas no rewrite rule anyway (#2312, #928, #285 are open FRs to add one) — the only effect of thepython -mpath is to route to the wrapped tool's filter.Related
[hooks] exclude_commandsis bypassed forhead/tail(line-range fast path skips the exclusion check) #2823 —exclude_commandsbypassed forhead/tail(line-range fast path returns before the check). Same family: the exclusion is not consistently applied to what rtk actually runs. That one returns before the check; this one runs the check against the wrong string.exclude_commandsdoesn't cover subcommands (git diff); also a root-token-matching limitation.exclude_commandsignores env-var-prefixed commands.