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.
- 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>
- **Path Centralization:** Hardcoded directory and file paths across all hook logic () are replaced with dedicated, exported constants in . This prevents magic strings and simplifies maintenance when system directories change.
- **Code Cleanup:** Move all code only used by tests behind cfg(test) attribute
- **Refactoring:** Apply Clippy fixes and address remaining warnings
Signed-off-by: Nicolas Le Cam <niko.lecam@gmail.com>
KuSh: the in-progress state line ("rebase in progress", "merge in
progress. unresolved conflicts", etc.) is enough — LLMs know which git
commands resolve each state, so the per-state hint list was noise.
Removes STATE_HINTS and extract_state_hint, and reduces extract_state_header
to a short walk that returns the first detected state summary or None.
Existing per-state tests now assert the exact compact summary.
- Drop redundant LANG=C env (LC_ALL=C is sufficient).
- Replace nested if/else chains in detect_status_state and
extract_state_hint with const slices + iter for readability.
- Drop the 2-space indent prefix on hint lines — adds nothing for
machine consumption.
Default `rtk git status` runs `git status --porcelain -b` to build its
compact view, but porcelain v1 omits the state header git prints when a
rebase, merge, cherry-pick, revert, bisect, am, or sparse checkout is in
progress. That header is correctness-critical — hiding "You are currently
editing a commit while rebasing..." leaves the user thinking the repo is
clean when it isn't.
`run_status` already captures plain `git status` output as `raw_output`
for tracking, so this fix adds `extract_state_header` to pull the state
block out of it and prepends the block to the compact output. Returns
`None` when nothing is in progress so clean repos are unchanged.
Covered states: interactive / regular rebase, merge-with-unmerged-paths,
"still merging after conflicts fixed", cherry-pick, revert, bisect, am,
sparse checkout. Preserves the directive hints git prints alongside
(`git rebase --continue`, `git commit --amend`, `git bisect reset`,
etc.), while still filtering generic `(use "git add")` / `(use "git
restore")` noise.
Design principle: Correctness vs Token Savings — never hide information
that changes the user's understanding of repo state.