Summary
When rtk rewrites a command (e.g. grep, find, ls) and the output is piped into another program or redirected to a file, the compressed human-readable format silently corrupts the downstream consumer. The caller usually has no way to detect this until a downstream assertion fails.
We suggest RTK follow the same convention as ls --color=auto, git diff, grep --color=auto, etc.: check isatty(stdout) at runtime, and fall back to raw passthrough whenever stdout is not a TTY (pipe, redirect, or process substitution).
Reproduction
Environment: rtk 0.36.0, Claude Code PreToolUse hook (rtk-rewrite.sh v3), Linux.
# A file with exactly 83 lines matching the pattern
$ grep -cE '^- \[ \]' TODO.md
83
# Naive redirection through RTK hook rewrite
$ grep -n '^- \[ \]' TODO.md > /tmp/pending.txt
$ wc -l /tmp/pending.txt
136 /tmp/pending.txt # <-- WRONG, expected 83
$ head -3 /tmp/pending.txt
83 matches in 36F:
// ... 134 more lines (total: 136)...
The file contains RTK's compressed human summary ("83 matches in 36F", "// ... 134 more lines", per-file headers like "[file] 921 (1):"), not raw grep output.
Using rtk proxy as a manual opt-out recovers correctness:
$ rtk proxy grep -n '^- \[ \]' TODO.md > /tmp/pending.txt
$ wc -l /tmp/pending.txt
83 /tmp/pending.txt # correct
Why this matters
-
Silent failure. The downstream consumer (wc -l, jq, diff, curl -d @file) sees syntactically valid but semantically wrong input. No error message, no exit code hint — the only signal is a factual discrepancy the caller may or may not notice.
-
Compounds through pipelines. If the corrupted output is fed to an LLM subagent (our case: classifying 83 TODO items via a MiniMax subagent), the downstream LLM receives garbage input and produces confidently wrong output. Correctness has to be verified via counts/diffs afterwards, because
the corruption leaves no exception trail.
-
AI agent risk. RTK's primary audience is LLM hooks (per the README). LLM agents frequently pipe grep/find output into jq/awk/other tools to build reasoning pipelines. A compression mode that is opt-out rather than opt-in is hostile to this use case.
Suggested fix
In the Rust code path that emits compressed output, add an IsTerminal check before applying compression:
use std::io::IsTerminal;
if std::io::stdout().is_terminal() {
print_compressed(output);
} else {
print_raw_passthrough(output);
}
std::io::IsTerminal is stable since Rust 1.70, so no external crate is needed.
This is the same contract as:
ls --color=auto
grep --color=auto
git diff (pager auto-detection)
cargo (coloring)
Users who explicitly want compression in a non-TTY context can still opt in via an env var (e.g. RTK_FORCE_COMPACT=1) or a rtk compact <cmd> subcommand.
Workaround we adopted
Until this is fixed upstream, we added a redirection/pipe detection layer to our rtk-rewrite.sh PreToolUse hook:
# Bypass RTK rewrite when the command shape indicates the output will be
# consumed by a program (file, pipe, substitution) instead of displayed.
if [[ "$CMD" =~ [\>\|\`] ]] || [[ "$CMD" == *'$('* ]] || [[ "$CMD" == *'<('* ]]; then
exit 0 # pass through unchanged — no RTK rewrite
fi
This works at the hook layer but does not help users who invoke rtk directly from an interactive terminal and then pipe it somewhere unexpectedly. An upstream fix is strictly better.
Design note
RTK's current default is compress unless opted out via rtk proxy. We would gently suggest the inverted default is safer: raw passthrough unless stdout is a TTY. Compression is valuable for humans reading a terminal; it is actively harmful for machines reading a stream. The TTY boundary is the
most reliable signal for which audience is on the other end.
Environment
- rtk version: 0.36.0
- OS: Linux 6.8.0-106-generic
- Shell: zsh / bash
- Consumer: Claude Code LLM hook (
rtk-rewrite.sh v3)
Happy to file a PR implementing the IsTerminal check if the maintainers agree with the direction.
Summary
When
rtkrewrites a command (e.g.grep,find,ls) and the output is piped into another program or redirected to a file, the compressed human-readable format silently corrupts the downstream consumer. The caller usually has no way to detect this until a downstream assertion fails.We suggest RTK follow the same convention as
ls --color=auto,git diff,grep --color=auto, etc.: checkisatty(stdout)at runtime, and fall back to raw passthrough whenever stdout is not a TTY (pipe, redirect, or process substitution).Reproduction
Environment: rtk 0.36.0, Claude Code PreToolUse hook (
rtk-rewrite.shv3), Linux.The file contains RTK's compressed human summary (
"83 matches in 36F","// ... 134 more lines", per-file headers like"[file] 921 (1):"), not raw grep output.Using
rtk proxyas a manual opt-out recovers correctness:Why this matters
Silent failure. The downstream consumer (
wc -l,jq,diff,curl -d @file) sees syntactically valid but semantically wrong input. No error message, no exit code hint — the only signal is a factual discrepancy the caller may or may not notice.Compounds through pipelines. If the corrupted output is fed to an LLM subagent (our case: classifying 83 TODO items via a MiniMax subagent), the downstream LLM receives garbage input and produces confidently wrong output. Correctness has to be verified via counts/diffs afterwards, because
the corruption leaves no exception trail.
AI agent risk. RTK's primary audience is LLM hooks (per the README). LLM agents frequently pipe grep/find output into jq/awk/other tools to build reasoning pipelines. A compression mode that is
opt-outrather thanopt-inis hostile to this use case.Suggested fix
In the Rust code path that emits compressed output, add an
IsTerminalcheck before applying compression:std::io::IsTerminalis stable since Rust 1.70, so no external crate is needed.This is the same contract as:
ls --color=autogrep --color=autogit diff(pager auto-detection)cargo(coloring)Users who explicitly want compression in a non-TTY context can still opt in via an env var (e.g.
RTK_FORCE_COMPACT=1) or artk compact <cmd>subcommand.Workaround we adopted
Until this is fixed upstream, we added a redirection/pipe detection layer to our
rtk-rewrite.shPreToolUse hook:This works at the hook layer but does not help users who invoke
rtkdirectly from an interactive terminal and then pipe it somewhere unexpectedly. An upstream fix is strictly better.Design note
RTK's current default is
compress unless opted out via rtk proxy. We would gently suggest the inverted default is safer:raw passthrough unless stdout is a TTY. Compression is valuable for humans reading a terminal; it is actively harmful for machines reading a stream. The TTY boundary is themost reliable signal for which audience is on the other end.
Environment
rtk-rewrite.shv3)Happy to file a PR implementing the
IsTerminalcheck if the maintainers agree with the direction.