#3427·rtk

With `--max-lines`, `-n` numbers the surviving lines 1..N (including the `[N more lines]` marker), so an agent told to edit "line 5" edits a different line; plus `--max-lines 0` emits brace soup and `-l minimal` drops the shebang

Author: HetCreepCreated Aug 4, 2026Updated Sep 16, 2026
Labelsbugpriority:higharea:clineeds-reproduction

Three edge cases in rtk read, all reproduced. The first is the one that can cause a wrong edit.

*Provenance: AI-assisted source audit, human-directed, at v0.44.2 (700bdde). Reproduced with the shipped binary; input and output verbatim. Extends #3421, which covers the same function's silent splicing.*

1. -n numbers the kept lines, not the file's lines

auth.rs on disk:

 1  fn can_admin(u: &User) -> bool {
 2      if !u.is_admin {
 3          return false;
 4      }
 5      if u.is_banned {
 6          return false;
 7      }
 8      true
 9  }
10  fn danger() {
11      wipe();
12  }
$ rtk read auth.rs --max-lines 8 -n
1 │ fn can_admin(u: &User) -> bool {
2 │     if !u.is_admin {
3 │         return false;
4 │     }
5 │     }
6 │ }
7 │ fn danger() {
8 │ [5 more lines]

Rows 1-4 happen to be correct. After that nothing lines up:

  • Row 5 shows }. Line 5 of the file is if u.is_banned {.
  • Row 7 shows fn danger() {. That is line 10 of the file; line 7 is }.
  • Row 8 numbers the [5 more lines] marker as though it were file content. There is no line 8 in this output at all.

An agent given this and asked to change line 5 edits the wrong line. src/cmds/system/read.rs:157 numbers the post-filter sequence rather than carrying each surviving line's original index.

Also visible here, and the reason #3421 exists: if u.is_banned { was dropped while its } survived, so the ban check reads as absent.

2. --max-lines 0 produces brace soup instead of nothing or an error

$ rtk rewrite head -0 auth.rs
rtk read auth.rs --max-lines 0

$ rtk read auth.rs --max-lines 0
fn can_admin(u: &User) -> bool {
    }
    }
}
fn danger() {
}

Six lines of orphaned braces for a request of zero lines. src/core/filter.rs:349 tests kept_lines >= max_lines - 1; with max_lines == 0 that subtraction underflows. Cargo.toml:45 sets panic = "abort" but not overflow-checks, so a release build wraps to usize::MAX and the break never fires — a debug build panics with attempt to subtract with overflow instead.

Reachable through the hook, since head -0 file rewrites to --max-lines 0.

3. -l minimal drops the shebang

$ cat s.sh
#!/usr/bin/env bash
set -euo pipefail
rm -rf "$TARGET"

$ rtk read s.sh -l minimal
set -euo pipefail
rm -rf "$TARGET"

filter.rs:202 treats #!/usr/bin/env bash as a # line comment. The output reads as a complete script but has lost the line that decides which interpreter runs it.

Suggested directions (untested — no programmer has reviewed these)

  1. Carry each kept line's original index through the filter and print that with -n; never number a synthetic marker line. If the original index is not available, omitting -n output entirely would be better than numbering that does not match the file.
  2. Handle max_lines == 0 explicitly — return empty, or reject it — rather than relying on max_lines - 1.
  3. Exempt a first-line #! from #-comment stripping.

Item 1 is the one I would prioritise: wrong line numbers are worse than no line numbers, because they look authoritative and an agent will act on them.