Native Windows has had full auto-rewrite support since v0.37.2 via the
`rtk hook claude` native binary command (no bash/jq/Unix shell). The
README still described the pre-v0.37.2 "CLAUDE.md fallback" behavior,
which is now inaccurate and a recurring source of confusion (#330).
- Rewrite the Windows section: native binary hook is the primary path,
WSL secondary; drop the "limited support / CLAUDE.md fallback" framing
- Add an upgrade note (re-run `rtk init -g` to migrate off rtk-rewrite.sh)
- Add a ripgrep prerequisite note (`rg` on PATH)
- Fix the install note and Supported AI Tools table (bash -> native binary)
Refs #330
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.claude/rules/cli-testing.md marks token-savings verification as a 🔴
requirement for filters. Add a ≥60% savings assertion over a realistic
paratest run (banner + config + parallel-worker progress → one-line
summary), alongside the existing behavioral tests.
paratest_cmd.rs lacked the mandatory #[cfg(test)] module, failing the
check-test-presence CI gate. Add tests exercising the ParaTest-specific
path in filter_test_runner_output: banner + "Random Seed:" + dot progress
stripped, result summary and failure details preserved.
uv run was listed in both BUILTIN_TRANSPARENT_PREFIXES and RULES,
causing the transparent prefix check to win and return None when the
inner command (e.g. python script.py) wasn't RTK-supported. Now all
uv run commands route through the RULES-based rtk uv path, which
delegates to uv_cmd.rs for filtering.
extract_json_object() used char indices from enumerate() as byte offsets
when slicing the input string. For multi-byte UTF-8 characters (CJK,
emoji), this causes a 'not a char boundary' panic because char index !=
byte offset.
Switch from Vec<char> + enumerate() to char_indices() which yields
correct byte offsets. Add regression tests for CJK values, emoji values,
CJK prefixes, and mixed multi-byte nested JSON.
Fixes#2509
- Fix documentation, Cursor now uses native rust binary hook
- Fix rtk-rewrite.sh shell script to fail on any exit code except 0 and 3
- Test that "continue": true is also present for "permission": "ask" and sync shell rewrite hook
composer_bin_dirs() re-read composer.json and the env on every call; the
rewrite hot path queries it several times per command segment via
normalize_php_tool_command (both classify_command and rewrite_segment_inner).
Resolution is constant for a process, so cache it in a OnceLock.
Matching only "by Sebastian Bergmann" misrouted any LICENSE, composer
metadata, or git log mentioning the author to the phpunit filter. Require
the input to start with "PHPUnit " as well.
Pest has no root `pest.php` file — its bootstrap lives at tests/Pest.php
and its canonical marker is the vendor/bin/pest binary. The root-level
check never matched real Pest projects and false-positived on unrelated
utility files, misrouting PHPUnit-only projects to the Pest filter.
`phpstan -c phpstan.neon analyse src/` put a global option before the
subcommand, so the first-arg-only check missed it and the run fell back
to unfiltered passthrough. Scan all args for analyse/analyze.
Replace the has_custom_format bool with a 3-state ErrorFormat enum so an
explicit `--error-format=json` is preserved without appending a second
`--error-format json`. Strip ANSI in the text fallback for `--ansi`.
filter_phpunit_output matched on raw bytes, so `--colors=always` output
(colorized "OK ("/"FAILURES!"/"Tests:" lines) defeated every anchor and
counts were lost. Strip ANSI first, mirroring the other PHP filters.
Also report PHPUnit errors (thrown exceptions) distinctly from failures
(assertion mismatches) instead of lumping both under "failures".
Interpret all RTK rewrite exit codes and respond appropriately:
- Exit 0 (Allow): auto-apply rewrite
- Exit 1 (Passthrough): no RTK equivalent, pass through unchanged
- Exit 2 (Deny): block the call entirely
- Exit 3 (Ask): rewrite available, require user approval via
requireApproval with allow-once/deny decisions
Uses execFileSync (not execSync) to avoid shell injection. Returns
a [string | null, RewriteVerdict?] tuple from tryRewrite so the
before_tool_call hook can react to each verdict type.
"allow-always" omitted from allowedDecisions because OpenClaw does
not auto-persist approval for plugin hooks — see:
https://docs.openclaw.ai/plugins/plugin-permission-requests#troubleshootingCloses#2202
PHP-CS-Fixer omits the fixers key in dry-run/diff modes (it is optional
in the JSON reporter), so a valid report like {"files":[{"name":"x.php"}]}
failed the whole parse and fell back to raw output. Mark the field
#[serde(default)] so a missing key deserializes to an empty vec.
Address PR #2609 review feedback:
- RC=3 in shell script now emits "permission": "ask" (future-proof)
- Add cursor_ask() and use it for all AskRewrite decisions
- Remove has_rules guard (unnecessary with ask semantics)
- Remove dead cursor_has_explicit_rules() function
Align cursor_has_explicit_rules() with the inline has_rules check in
run_cursor_inner_with_rules() — both now consider deny, ask, and allow
rules when determining whether the user has configured explicit
permission policies.
When no explicit permission rules exist in ~/.cursor/cli-config.json
(the default for fresh installs), every command gets Default verdict
which maps to AskRewrite. The Cursor hook only handled AllowRewrite,
silently dropping all rewrites and making RTK non-functional.
Now treat AskRewrite as allow when no rules are configured — Cursor
has no ask-the-user UX, so deferring is indistinguishable from
dropping. When explicit rules exist, preserve the conservative
behavior of deferring mixed/unmatched commands.
Also fix the legacy shell script (rtk-rewrite.sh) which treated
exit code 3 (ask) as failure via || short-circuit.
Fixes#2372
The six Composer-tool rules had inconsistent patterns, and their
rewrite_prefixes only worked because classify normalizes the command while
rewrite stripped literal prefixes off the raw text. A form the pattern
accepted but the prefix list missed (e.g. ./bin/phpunit) would classify yet
silently fail to rewrite.
rewrite_segment_inner now normalizes the leading invocation for these tools
(php wrapper, ./, vendor/bin, composer bin-dir) the same way classify does, so
the prefix list collapses to the residual canonical forms: bin/<tool> + bare
name for phpunit/phpstan, bare name for pest/paratest/ecs/pint. Patterns
standardized to ^(?:php\s+)?(?:\./)?(?:(?:vendor/)?bin/)?<tool> for
phpunit/phpstan and ^(?:\./)?(?:vendor/bin/)?<tool> for the rest. Adds a
form-coverage test asserting every accepted spelling maps to one rewrite.
is_numbered_failure_heading matched any line starting with a digit and
containing ')', which split a failure block on detail lines like
"5 of 10 assertions passed in Foo::bar()". Anchor to `^\d+\) \S` via a
lazy_static regex. Adds a regression test.
- run() now uses php_tool_command("phpstan") instead of hardcoding
vendor/bin/phpstan, so COMPOSER_BIN_DIR and composer.json's config.bin-dir
are respected, matching every other php tool in this module.
- filter_phpstan_text only treats shell-level "binary missing" lines as a
failure. The previous contains("not found") matched real analysis messages
("Class X not found") and swallowed the summary; also dropped a stray Ruby
LoadError string ("cannot load such file"). Adds a regression test.
- compact_php_path reduced to last-two-components for all frameworks, dropping
the Laravel-specific prefix list. Tests updated to the compacted output.
The `.semgrep.yml` `dynamic-command-execution` rule forbids `Command::new` on a
variable. `php_tool_command` built the command directly from the resolved tool
path; route it through `resolved_command` (the sanctioned PATHEXT-aware
constructor) instead. Clears both blocking findings with no behavior change.
develop's `test_every_subcommand_is_classified` requires every CLI subcommand
to appear in `RTK_META_COMMANDS` or `PASSTHROUGH`. The consolidated php
subcommands (php, phpunit, phpstan, pest, paratest, ecs, pint) wrap real
tools, so they belong in `PASSTHROUGH`. Without this the PR-into-develop merge
fails the test.
`./vendor/bin/<tool>` is the common Laravel invocation form. classify_command
normalizes the leading `./`, so these classify as supported, but the rewrite
strips literal `rewrite_prefixes` from the raw command and the five rules only
carried `vendor/bin/<tool>` and bare `<tool>`. So `./vendor/bin/pint` ran raw
with no compression while `vendor/bin/pint` rewrote to `rtk pint`. phpstan
already carried `./vendor/bin/phpstan`. Add the `./vendor/bin/` prefix to the
other five, with a regression test.
Reported by @evaldnet (verified against pint 1.29.1, phpstan 2.1.40).
Co-authored-by: Aaron Florey <azza@jcaks.net>
Co-authored-by: Eli White <1153183+EliW@users.noreply.github.com>
Co-authored-by: Benjamin LETELLIER <bletellier@audencia.com>
Co-authored-by: Luciano <vandi.luciano@gmail.com>
pint: Pint >=1.14 renamed JSON keys name->path and appliedFixers->fixers.
The struct fields were required with no aliases, so serde rejected current
output and the filter fell back to raw (no compression). Add backward-
compatible aliases so both schemas parse.
phpstan: the "ok" gate and summary line read totals.errors, which counts
only non-file-specific (global) errors. A normal failing run reports
errors=0 with the count in file_errors, so runs with real errors were
reported as "phpstan: ok", silently hiding failures. Gate on both counts
and report file_errors in the summary.
Both regressions slipped past the suite because the fixtures set
errors == file_errors (phpstan) and used the old key names (pint). Added
regression tests using the current-version schemas.
Reported by @evaldnet (verified against Pint 1.27.1, PHPStan 2.1.40).
Co-authored-by: Aaron Florey <azza@jcaks.net>
Co-authored-by: Eli White <1153183+EliW@users.noreply.github.com>
Co-authored-by: Benjamin LETELLIER <bletellier@audencia.com>
Co-authored-by: Luciano <vandi.luciano@gmail.com>
The PHP runners (phpunit, pest/paratest, ecs, phpstan, pint) only
applied their compact filters when rtk itself launched the process.
Output produced elsewhere — most commonly a tool run inside a Docker
container and piped back to the host — bypassed them entirely, since
the visible command is `docker ...`, not the PHP tool.
Wire the existing filter functions into `rtk pipe`:
- resolve_filter: phpunit, pest|paratest|php-test, ecs, phpstan, pint
- phpstan/pint pipe wrappers sniff JSON-vs-text by content, since the
runners force --format=json but piped output may be either
- auto_detect_filter: route the "by Sebastian Bergmann" banner to the
phpunit filter (no -f needed)
- bump the four backing fns to pub(crate)
Also fix filter_phpstan_text matching the summary line case-sensitively
("found"), which missed phpstan's actual "[ERROR] Found N errors".
Tests: phpunit banner auto-detect, phpstan case-insensitive summary.
Upstream develop changed `rewrite_command` to take 3 args
(cmd, excluded, transparent_prefixes). The PHP tooling tests that
don't care about transparent prefixes now use the existing
`rewrite_command_no_prefixes` helper instead.
`Commands::Run` used `status.code().unwrap_or(1)` which silently
maps signal kills (SIGTERM, SIGKILL, OOM) to exit code 1 instead
of the POSIX-conventional 128+signal. Use the existing
`exit_code_from_status()` helper already used by Commands::Proxy.
Fixes#2680
- drop the synthetic "search failed with exit code N" line (an rtk addition)
- surface the engine stderr verbatim in every path, incl. partial match + error
- remove the now-vestigial is_grep_error_exit (the "0 matches" it guarded is gone)
- error/exit matrix tests vs real grep/rg (#2465: an error is never a silent no-match)
- grep_compress_test.rs -> search_compress_test.rs (tests the shared grep/rg filter)
- binary match skipped as noise by default, -a opts back in
- fix parse_flags doc: -I is an intentional binary skip, not a no-op
- leading ^ anchor must stay scoped to the path (tenequm)
- line ending in `:` keeps full content (BTCAlchemist)
- rg surfaces a regex parse error as exit 2, not a silent 0 (jhagberg)
- line number always (the openable position); filename only when grep/rg prints one (multi-file, dir, -r, -H)
- single file and stdin equal `grep -n` byte-for-byte: no synthetic filename, header, or lossy truncation
- read piped stdin instead of injecting `.`; rg no longer searches the cwd on a pipe
- pattern-less and format-flag commands run verbatim (rg --type-list, --files, grep -c/-o)
- grouped/capped form only when capping actually shrinks the output
- byte-for-byte tests vs grep -n across flags, pipes and edge cases (incl #1436)