Repository navigation
rtk compacts machine-bound git output (--porcelain / --name-only / piped), silently corrupting consumers #2487
Description
Summary
The Claude Code rewrite hook turns git X into rtk git X and rtk git
compacts the output. That's the right behavior when the model reads
the output. But rtk also rewrites git commands whose output is bound for
a program — piped into another command, or carrying an explicit
machine-readable format flag — and there its compacted, non-byte-exact
output silently corrupts the consumer.
This is not a request to weaken compaction. rtk already draws a
model-vs-machine line (see below); it's just drawing it in the wrong
place for a few cases.
Evidence that rtk already distinguishes machine output
git rev-parse is treated as machine data and passed through unchanged:
$ rtk rewrite 'git rev-parse HEAD' # exit 1 → pass through
$ rtk rewrite 'git rev-parse HEAD | head -1' # exit 1 → pass through
So "don't rewrite this git command" is already a first-class outcome in
the registry. The bug is that the same courtesy isn't extended to other
machine-output cases.
The mis-drawn cases
rtk rewrites (and thus compacts) commands that explicitly request
parseable output or feed a parser:
$ rtk rewrite 'git status --porcelain | wc -l' → rtk git status --porcelain | wc -l
$ rtk rewrite 'git log --format=%H | sort' → rtk git log --format=%H | sort
$ rtk rewrite 'git diff | wc -l' → rtk git diff | wc -l
--porcelain is literally git's stable machine-readable contract;
--format=%H / --name-only / -z are the same intent. Compacting
them overrides an explicit "this output is for a program" signal.
Concrete corruption — rtk appends a --- Changes --- footer to diff
output, so a file-list count is wrong:
$ rtk proxy git diff --name-only HEAD~1..HEAD | grep -c . # raw git
17
$ rtk git diff --name-only HEAD~1..HEAD | grep -c . # via rtk
20
$ diff <(rtk proxy git diff --name-only HEAD~1..HEAD) \
<(rtk git diff --name-only HEAD~1..HEAD)
17a18,20
>
> --- Changes ---
>
Secondary, intermittent: the first rtk git diff ... in a fresh
shell sometimes returns empty output (looks like a cold-cache miss);
repeated identical calls then return the full (footer-decorated) result.
... | wc -l → 1 when the truth is 17.
All of these fail silently — a plausible wrong number, no error.
Anything scripted that counts/parses git output gets bad data.
Environment
- rtk 0.36.0, git 2.53.0, macOS (Darwin 25.5.0)
- Hook:
rtk-rewrite.sh→rtk rewriteprependsrtkto git commands
Proposed fix
Extend rtk's existing pass-through logic (the rev-parse exit-1 path)
to decline rewriting when the command signals a machine consumer:
- an explicit machine-format flag is present —
--porcelain,--format=/--pretty=format:,--name-only,
--name-status,-z,--no-color; or - the git command's stdout is piped into another command or redirected
into a file (rtk rewritealready has the full command string, so a
trailing| <cmd>/> fileafter the git call is detectable).
In those cases, emit the command as raw git. Keep compaction fully on
for the common, high-token, model-facing case (a bare git diff /
git status / git log whose output the model reads directly). Token
savings are essentially unchanged; correctness for programmatic
consumers is restored.
Separately, the intermittent empty-output-on-first-call should be fixed
regardless — empty is never a valid compaction of a non-empty diff.
Workaround (current)
rtk proxy git ... to bypass the wrapper, or append | cat.
Activity
pszymkowiak commentedon Jun 25, 2026
🔍 Automated Triage
bughighSummary
rtk applies compaction to git output even when machine-readable flags (--porcelain, --format=, --name-only, -z) or pipes are present, silently corrupting consumers with wrong counts and injected footers; PR #2573 already in progress.
Triaged automatically by wshm · This is an automated analysis, not a human review.
fix(git): keep machine-bound output raw (--name-only/--porcelain/piped)
fix(git): keep machine-bound output raw (--name-only/--porcelain/piped)
fix(git): keep machine-bound output raw (--name-only/--porcelain/piped)
macblackstuff commentedon Aug 1, 2026
Still present on 0.44.1 (macOS, Homebrew), and there is a sharper minimal case than the
--porcelainones above: an empty result becomes one line.git log --oneline -0prints nothing.rtk gitprints its status line on stdout, so anything counting lines sees 1 where the real answer is 0.Why this one is worse than truncation: the corruption is not a compressed variant of the right answer, it is the inverse of it. Every emptiness test built on this shape silently flips:
We hit exactly that — a session reporting unpushed work on a fully pushed branch — and it took a while to trace, because both the command and the pipeline are correct and no error is emitted anywhere.
Two observations that may help scope a fix:
rtk proxyis already correct here (0), so the passthrough path is sound — the divergence is only in what the wrapped path writes to stdout.Our house workaround, in case it is useful as a data point on the cost side: our agent instructions now carry a standing "never pipe rtk-wrapped output into a line counter" rule with a list of safe substitutes (
git rev-list --count,rtk proxy). It works, but it is a rule every agent has to remember on every call, and the failure is silent when they forget.febpetroni commentedon Sep 28, 2026
All three "mis-drawn cases" in the description now pass through on 0.50.0. The underlying principle still holds, but via a different family — so the repro block is stale and someone trying to reproduce it will conclude the issue is invalid.
Environment: rtk 0.50.0, Darwin 25.3.0 arm64, hook-version 3.
All three are exit 1 (pass through), matching the
git rev-parsecontrol the description cites as the correct behavior. Redirections are covered too:That last one mattered: on 0.34.2 it rewrote to
rtk read file.txt > out.txt, which is not a byte-exact copy — I measured 200 B of binary in, 0 B out, exit 0.What still reproduces is the same principle with a positional consumer instead of an aggregating one:
--porcelainis an explicit machine-format flag, so this is exactly the case the description argues against — it just now survives only throughhead/tail, which #4207 and #4026 already own.Suggestion: either close this and let #4207/#4026 carry the remainder, or swap the repro block for the
head/tailforms above so the issue stays reproducible. Happy to do whichever you prefer.sholsinger commentedon Sep 30, 2026
Still happening on 0.49.0, and the symptom is worse than the footer case: the patch body is gone entirely while the header still claims it.
The file had a real one-line change. A
Changes:header with nothing under it is worse than empty output, because it reads as "I looked, and there's nothing there." Two identical invocations in a row both lost it, andrtk proxy git diffshowed the patch every time.I couldn't turn this into a reproduction. A fresh repo with the same filename and content behaves correctly, and recreating the same change in the same repo later behaved correctly too. That fits the cold-cache note above, so I assume it's the same bug rather than a new one.
Environment: rtk 0.49.0, git 2.50.1 (Apple Git-155), Darwin 24.6.0, Claude Code rewrite hook.
I'm happy to capture cache or state the next time it happens. Tell me what would be useful and I'll grab it.