`rtk init -g --agent cursor` still tried to write ~/.claude/RTK.md
because install_claude was derived from `!opencode` only, ignoring the
explicit non-Claude agent selection. Users with no prior Claude Code
install hit:
Failed to write RTK.md: ~/.claude/RTK.md: No such file or directory
Fix:
- Route install_claude through a `should_install_claude(opencode, agent)`
helper so `--agent cursor|windsurf|cline|kilocode|antigravity` skips
the Claude artifacts (RTK.md, CLAUDE.md patch, settings.json hook).
`--agent claude` and no-agent default continue to install Claude.
- Have `patch_cursor_hooks_json` create `~/.cursor/` on demand so
Cursor-only users without that directory don't fail.
Tests:
- `test_should_install_claude_routing`: exhaustive matrix over
(opencode, agent) combinations.
- `test_patch_cursor_hooks_json_creates_missing_parent_dir`: regression
guard ensuring the parent directory is auto-created.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three related issues prevented RTK from working with the Cursor
preToolUse hook on Windows. Each one alone produced the same
user-visible symptom (`Output: {}` and the original command running
unmodified), so they're fixed together:
1. Cursor preToolUse only enforces allow/deny — `permission: "ask"` is
accepted by the schema but ignored at runtime, so `updated_input`
rewrites could silently be dropped. Always return `allow` for
rewritten commands (deny rules still take precedence and are
evaluated before this point).
2. Cursor's preToolUse panel renders the JSON returned by the hook.
Without a `continue: true` field the panel collapses to
`Output: {}` even when the rewrite ran successfully, which makes
the hook look broken from the user's perspective. Every other
Cursor hook (`afterShellExecution`, `beforeSubmitPrompt`, `stop`,
...) returns `continue: true`; mirror that shape here so the panel
surfaces the actual `permission` and `updated_input` payload.
3. Cursor on Windows prepends one or two UTF-8 BOMs (`EF BB BF`,
sometimes doubled) to the JSON it pipes into the hook process.
serde_json refuses to parse BOM-prefixed input, so `run_cursor`
bailed out into the "no command" branch and returned `{}` for
every invocation. Strip leading BOMs before parsing. This was the
root cause of the long-standing "RTK silently no-ops in Cursor"
reports — the rewrite path was never reached.
Tests cover the flat allow output, `continue: true` on both single
and compound (`cd ... && git status`) rewrites, the BOM-strip on
double-BOM payloads matching what Cursor actually sends, and assert
that the legacy hookSpecificOutput envelope is not emitted.
Verified end to end on Windows by tracing real Cursor stdin payloads:
the hook now returns `{"continue":true,"permission":"allow",
"updated_input":{"command":"rtk git status"}}` for `git status` and
the panel renders it instead of `Output: {}`.
When -bench is present, benchmark output is already compact and useful.
Injecting -json causes the filter to discard benchmark results since they
don't produce pass/fail events the JSON parser expects.
Fixes#1609
Command wrappers like shadowenv (https://github.com/Shopify/shadowenv),
`direnv exec .`, `nix develop --command`, `docker exec <container>`,
`poetry run`, and `bundle exec` sit in front of every command in their
respective project layouts. When rtk is driving a Claude Code / Gemini /
Copilot / opencode hook, these wrappers defeat the rewrite — `rtk
rewrite "shadowenv exec -- git status"` returns exit 1 (no rewrite)
because the registry can't see past the wrapper to the inner `git
status`. Every build tool, test runner, linter, and `git` invocation
in such a layout stays uncompressed.
rtk already solves exactly this shape for shell builtins via
SHELL_PREFIX_BUILTINS (`noglob`, `command`, `builtin`, `exec`,
`nocorrect`): strip the prefix, recurse on the inner command, re-prepend
the prefix to the rewrite. This commit extends that pattern with a new
[hooks].transparent_prefixes config field so users can register
multi-word wrappers that don't change what runs, only how.
[hooks]
transparent_prefixes = ["shadowenv exec --", "direnv exec ."]
With that config, `rtk rewrite "shadowenv exec -- cargo test"` becomes
`shadowenv exec -- rtk cargo test` and exits 3 (ask, matching the
existing ask semantics for inner commands).
Implementation:
- New `rewrite_command_with_prefixes(cmd, excluded, transparent_prefixes)`
public entry. The original `rewrite_command(cmd, excluded)` is kept as
a thin back-compat wrapper that passes an empty prefix slice, so the
~150 existing unit-test call sites don't need updating.
- `transparent_prefixes: &[String]` threaded through `rewrite_compound`,
`rewrite_segment`, `rewrite_segment_inner`.
- In `rewrite_segment_inner`, a second strip loop runs after the
SHELL_PREFIX_BUILTINS loop, with identical strip-recurse-reprepend
semantics. Matching is whole-word via the existing `strip_word_prefix`
helper (multi-word prefixes like `"shadowenv exec --"` are handled
by-value — byte-level `starts_with` plus a space check).
- Recursion is bounded by the existing MAX_PREFIX_DEPTH so pathological
or self-referential configs cannot stack-overflow.
- Production callers (`hooks/rewrite_cmd.rs`, `hooks/hook_cmd.rs`,
`main.rs`) now load both `exclude_commands` and `transparent_prefixes`
from config in the same `Config::load()` call — no new I/O.
Tests (14 new):
- config: 3 TOML roundtrip tests (present, missing, mixed with older
fields).
- registry: 11 behavior tests — strip/reprepend, unknown inner returns
None, unmatched passthrough, composed with shell builtin, multiple
prefixes configured, whole-word matching, empty-rest None, blank-entry
skip, compound `&&` with prefix on both segments, excluded-inner
returns None, recursion bounded.
Design principles (from CONTRIBUTING.md):
- Extensibility: extends an existing, documented pattern.
- Zero Overhead: config load already happens on every hook invocation;
this reads one additional vec. Empty `transparent_prefixes` is the
default and adds one cheap loop-over-zero-elements on the hot path.
- Correctness vs Token Savings: without this, hook rewrites silently
miss every command run through a wrapper — exactly the kind of
correctness gap the principle warns against.
The file lives at docs/contributing/ARCHITECTURE.md, not at the repo
root — both the nav header and Documentation section pointed to the
wrong location.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Separate warnings and errors into distinct sections.
- Ensure status line is emitted last for clarity.
- Maintain consistency with native `dotnet` output behavior.
The `ok` / `fail` verdict line was emitted at the top of the filtered
output, before errors and warnings. Consumers reading the tail of the
stream — `| tail -N`, IDE log followers, agent watch/monitor loops,
bounded-context-window readers — saw error noise followed by EOF with
no verdict anywhere near the end, silently breaking any agent loop that
gates the next step on build success.
Reorder `format_build_output`, `format_test_output`, and
`format_restore_output` to emit the body first, then the separator
(only when the body is non-empty), then the verdict header last,
matching native `dotnet` which ends with `Build succeeded.` or
`Build FAILED.`.
Add three regression tests using strict `output.lines().last()` plus a
`tail -5` inclusion assertion so future regressions that append a
trailing context line get caught.
Closes#1574
Signed-off-by: Artiom Tofan <arto@queue-it.com>
new_gradle_command() now uses resolved_command("gradle") for the two
fallback branches (Windows + Unix when no local wrapper is present),
matching how cargo / golangci-lint / etc. resolve system binaries.
Local wrappers (./gradlew, gradlew.bat) stay as string literals — they
are relative paths, not on PATH, and semgrep's dynamic-command-execution
rule needs literals here.
Net effect: PATHEXT-aware resolution on Windows (.CMD/.BAT shims work),
no behavioural change on Unix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extract a new_gradle_command() helper that uses string literals in
every Command::new() branch. The .semgrep.yml dynamic-command-execution
rule rejects Command::new(var) — semgrep needs to statically audit
the executable set.
Same runtime behaviour: prefer ./gradlew (or gradlew.bat on Windows),
fall back to gradle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds rtk gradlew command for build, test, lint, and dependency
operations on Gradle projects. Filters task progress noise, preserves
build scan URLs, test failures, lint violations, and compiler warnings.
Recognises ./gradlew, gradlew, gradlew.bat, and gradle invocations.
Surfaces unit-test report paths and shows a progress indicator for
long-running tasks.
Targets 75% savings (90% on test, 80% on build).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Character devices (c), block devices (b), pipes (p), and sockets (s)
were silently dropped by compact_ls because only regular files (-),
symlinks (l), and directories (d) were handled. This caused
`rtk ls /dev/ttyACM*` to return "(empty)".
Closes#844
Follow-up to the initial #1536 fix after a deeper review pass.
- force_tee_hint() is no longer called on the passthrough path. Previously
every non-TTY pipe invocation (curl | jq, curl | python -m json.tool —
the exact scenarios this PR fixes) was still writing a tee log file as a
side effect, which is wasteful: the tee file's purpose is to give the
LLM a recovery path when output is truncated, and we are no longer
truncating in those cases.
- looks_like_json now also matches bare top-level JSON strings (e.g.
/api/token endpoints returning a long quoted token). Previously a
>500-byte bare JSON string on a TTY would still get truncated mid-stream
and produce invalid output.
- FilterResult.content is now Cow<'_, str> so the passthrough path no
longer copies the whole body. For multi-MB curl responses piped through
rtk this eliminates a full-size allocation.
- Exit code from curl is propagated via Ok(result.exit_code) instead of
the previously hardcoded Ok(0). On the success path the value is 0
anyway, but the inconsistency with the early-exit branch above made the
intent unclear.
Tests: 9 -> 11 (added bare-JSON-string passthrough and a Cow::Borrowed
assertion verifying the passthrough paths don't allocate). cargo test
--all: 1693 passed, 6 ignored.
Verified end-to-end against jsonplaceholder.typicode.com/posts (~27 KB,
100 entries, parseable) and a randomly-generated 100-entry / 52 KB / 1450-line
JSON served from a local http.server (rtk output byte-identical to source
modulo the trailing newline added by println!).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`rtk curl` was unconditionally truncating any response >=500 bytes when a
tee hint was available, inserting the literal `... (N bytes total)` marker
mid-stream. For JSON bodies this produces invalid, unparseable output and
silently breaks every downstream `jq`, `python -m json.tool`, or agent
pipeline that consumes `rtk curl`.
Two changes in `filter_curl_output`:
1. JSON heuristic passthrough — if the trimmed body starts/ends with
matching JSON brackets, return it unchanged. The body is already
buffered in memory so the check is essentially free.
2. `is_terminal()` gate — only truncate non-JSON output when stdout is
a TTY. Pipes and shell redirects (`> file`, `| jq`) now receive the
full body, matching what `curl -o file` already does and closing
the silent-data-loss path described in #1282.
The tee file is still written for inspection, but the hint line is
suppressed in the passthrough cases so it never leaks into pipes.
Tests: existing 5 cases updated to pass `is_tty=true` (preserve TTY-mode
assertions); 4 new cases cover JSON object/array passthrough at
>500 bytes and pipe-mode (non-TTY) full-body delivery.
Verified end-to-end against the public repro from the issue:
`rtk curl -s https://jsonplaceholder.typicode.com/posts | python3 -c
'import sys,json; print(len(json.load(sys.stdin)))'` now returns 100.
Closes#1536
Refs #1282
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>