rtk-ai/rtk

rtk rewrite mishandles newline-separated (multi-line) commands → only first line rewritten / empty output

Open

#2,191 opened on Jun 1, 2026

View on GitHub
 (1 comment) (0 reactions) (0 assignees)Rust (48,085 stars) (2,914 forks)batch import
area:clibughelp wantedneeds-reproductionpriority:high

Description

Summary

rtk rewrite does not treat newline (\n) as a statement separator. It appears to only parse/rewrite the first newline-delimited segment of a command, which breaks multi-statement commands written across multiple lines:

  • If the first line is not a rewritable command (cd, echo, …), rtk rewrite returns empty output with exit 1 ("no rewrite"), even when later lines contain rewritable commands (grep, tsc, …).
  • If the first line is rewritable, only that first line is rewritten; rewritable commands on subsequent lines are left untouched.

Semicolon-separated equivalents are handled correctly. So the bug is specifically newline handling.

Combined with the official Claude Code hook (rtk-rewrite.sh, which applies the rewrite when rtk rewrite exits 0/3), this surfaces as silently truncated output when an agent runs a multi-line shell snippet containing several greps — only the first section is produced. This was hit repeatedly in a real Claude Code session.

Environment

  • rtk 0.42.0 (Homebrew, macOS arm64) — also reproduced on 0.35.0
  • Claude Code PreToolUse hook rtk-rewrite.sh (# rtk-hook-version: 3)

Reproduction (deterministic, no Claude Code needed)

printf 'x\n' > /tmp/f.txt

# (1) Semicolon-separated — CORRECT (both greps rewritten):
rtk rewrite 'cd /tmp; grep -c x /tmp/f.txt; grep -c x /tmp/f.txt'
#   -> cd /tmp; rtk grep -c x /tmp/f.txt; rtk grep -c x /tmp/f.txt      (exit 0)

# (2) Same commands, newline-separated, first line is `cd` — BUG: empty + exit 1
rtk rewrite "$(printf 'cd /tmp\ngrep -c x /tmp/f.txt\ngrep -c x /tmp/f.txt')"
#   -> (empty)                                                          (exit 1)

# (3) Newline-separated, first line is grep — BUG: only FIRST line rewritten
rtk rewrite "$(printf 'grep -c x /tmp/f.txt\ngrep -c x /tmp/f.txt')"
#   -> rtk grep -c x /tmp/f.txt
#      grep -c x /tmp/f.txt          <- second grep left un-rewritten   (exit 0)

Bisection — every multiline form whose first line is non-rewritable returns exit=1, len=0:

command result
cd /tmp; grep…; grep… (semicolons) exit 0, full rewrite ✓
grep…\ngrep… (newlines, grep first) exit 0, only 1st line rewritten
cd /tmp\ngrep…\ngrep… exit 1, empty
cd /tmp\necho A\ngrep…\necho B\ngrep… exit 1, empty
echo 中文\ngrep…\ngrep… exit 1, empty
cd "/tmp/a b"\ngrep…\ngrep… exit 1, empty

Expected

rtk rewrite should treat \n as a statement separator (like ;), rewriting each rewritable statement independently and preserving every statement in the output — so a newline-separated command yields the same result as its semicolon-separated equivalent.

Impact

Multi-line shell snippets are extremely common in agent/hook usage (heredocs, multi-step verification blocks). Today they are either:

  • left with only the first statement token-optimized (mild), or
  • effectively pass-through/empty depending on the first line (and, via hooks that act on the rewrite, produce truncated/partial command execution with no error) — silent and easy to mistake for a tool/agent bug.

Suggested fix

In the rewrite parser (registry), split the command on top-level statement separators including newline (\n) in addition to ; / && / ||, rewrite each segment, and re-join preserving the original separators.

Contributor guide