Split out of #552, which was closed by #573 (2026-09-12T17:30:31Z). #573's measured scope was ~13 tabwriter renderers and it deliberately excluded free-text surfaces — its own message says those "deliberately keep safeTerm + indentContinuation — they are not cells." internal/cmd/download.go is not on #552's table and was never covered by it.
So the residual below is real, documented in the tree, and as of #573 had no open tracking object. Found by the round-2 audit of #572.
The residual
internal/saferune deliberately retains \n and \t (Cc, minus \n and \t). safeTerm therefore does not stop a server-supplied files[].name containing a newline from forging whole lines on four surfaces that #572 otherwise hardened:
| site |
surface |
download.go (*progressWriter).line |
the \r-rewritten progress line, re-emitted at 10 Hz on a TTY |
download.go checkTargetCollisions |
the same-target overwrite refusal — forged - [id N] rows inside the group listing |
download.go downloadOne |
the SHA256-mismatch message, i.e. the CLI asserting an integrity failure |
download.go downloadStatusError |
the 401/403/404 arms |
Measured with files[].name = "weights.safetensors\nSaved /home/u/legit.safetensors (4.0 GiB) (SHA256 verified)": the progress line renders the forged Saved … (SHA256 verified) before any transfer completes, and done() leaves it standing.
Not a regression. Every one of these surfaces was fully raw before #572; that PR is a strict improvement and states this residual rather than hiding it. What was missing is an owner.
Why it is not simply "apply safeTermSingle"
These are free-text lines, not tabwriter cells, so #573's cell rule does not apply to them unmodified: collapsing \n to a space in a \r-rewritten progress line and in a multi-line refusal has different display consequences than it does in a cell, and the refusal is a fail-safe whose group structure must stay readable. Deciding which transform each surface takes is the work here — this issue does not prescribe one.
Correction to this issue's first draft
The original closing condition below said the four in-tree statements were "three comment blocks in download.go and the README Troubleshooting row". That was wrong, and #572's round-2 fix pass measured it: the README row carries no issue pointer at all. The fourth statement is the header paragraph of internal/cmd/download_safeterm_test.go. Left visible rather than silently edited, since the miscount would have let the condition read as satisfied while a statement still pointed at a closed issue.
Those four are already repointed at this issue by #572, so that half of the condition is discharged when #572 merges.
Closing condition
The four surfaces in the table above render a server-supplied name containing \n (and \t) as a single terminal line, and a test drives each one with a newline-bearing name and asserts the rendered output occupies the expected number of lines — failing if a forged line appears.
Checked by: go test ./internal/cmd -count=1 exits non-zero when the newline transform is removed from (*progressWriter).line, naming that function rather than a build error.
Split out of #552, which was closed by #573 (2026-09-12T17:30:31Z). #573's measured scope was ~13 tabwriter renderers and it deliberately excluded free-text surfaces — its own message says those "deliberately keep safeTerm + indentContinuation — they are not cells."
internal/cmd/download.gois not on #552's table and was never covered by it.So the residual below is real, documented in the tree, and as of #573 had no open tracking object. Found by the round-2 audit of #572.
The residual
internal/saferunedeliberately retains\nand\t(Cc, minus \n and \t).safeTermtherefore does not stop a server-suppliedfiles[].namecontaining a newline from forging whole lines on four surfaces that #572 otherwise hardened:download.go(*progressWriter).line\r-rewritten progress line, re-emitted at 10 Hz on a TTYdownload.gocheckTargetCollisions- [id N]rows inside the group listingdownload.godownloadOnedownload.godownloadStatusErrorMeasured with
files[].name = "weights.safetensors\nSaved /home/u/legit.safetensors (4.0 GiB) (SHA256 verified)": the progress line renders the forgedSaved … (SHA256 verified)before any transfer completes, anddone()leaves it standing.Not a regression. Every one of these surfaces was fully raw before #572; that PR is a strict improvement and states this residual rather than hiding it. What was missing is an owner.
Why it is not simply "apply safeTermSingle"
These are free-text lines, not tabwriter cells, so #573's cell rule does not apply to them unmodified: collapsing
\nto a space in a\r-rewritten progress line and in a multi-line refusal has different display consequences than it does in a cell, and the refusal is a fail-safe whose group structure must stay readable. Deciding which transform each surface takes is the work here — this issue does not prescribe one.Correction to this issue's first draft
The original closing condition below said the four in-tree statements were "three comment blocks in
download.goand the README Troubleshooting row". That was wrong, and #572's round-2 fix pass measured it: the README row carries no issue pointer at all. The fourth statement is the header paragraph ofinternal/cmd/download_safeterm_test.go. Left visible rather than silently edited, since the miscount would have let the condition read as satisfied while a statement still pointed at a closed issue.Those four are already repointed at this issue by #572, so that half of the condition is discharged when #572 merges.
Closing condition
The four surfaces in the table above render a server-supplied name containing
\n(and\t) as a single terminal line, and a test drives each one with a newline-bearing name and asserts the rendered output occupies the expected number of lines — failing if a forged line appears.Checked by:
go test ./internal/cmd -count=1exits non-zero when the newline transform is removed from(*progressWriter).line, naming that function rather than a build error.