CLI argument parsing: fail loudly on malformed ssh:// URLs; support vim-style +N line args - #2951
Merged
Conversation
A userless `ssh://host/path` URL run without $USER/$USERNAME in the environment (and any other malformed ssh:// argument: missing path, bad port, empty user) silently fell back to opening a LOCAL buffer literally named `ssh://...`. No SSH connection was attempted and no error shown; typing into the buffer and saving offered to create a local `ssh://host/...` directory — a data-loss trap for users following the documented URL form (#2221). The `ssh://` scheme is now claimed exclusively by the remote parser: parse_location returns a Result, and anything starting with `ssh://` that does not parse as a remote spec fails startup with a clear reason (e.g. "no user given and neither $USER nor $USERNAME is set"). The default user is injected as a parameter so unit tests cover the no-$USER case without racing over process environment. Local files with odd names (ssh:file, ./ssh://x) are untouched — only the exact scheme prefix is claimed, and the error suggests the ./ escape hatch. Fixes #2221 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
`fresh +50 file.txt` used to open a stray empty buffer literally named `+50` next to the real file — and in trailing position (`fresh file.txt +50`) that stray buffer also stole focus. Users coming from vi/vim expect +N to open the file at that line (#1926). `+N` arguments are now folded into the file list before any consumer (TUI, GUI, web, attach, nested forward) sees it: the line is applied to the first file argument through the exact same `file:line` syntax path that already existed, so `fresh +50 file.txt` == `fresh file.txt:50`, including for remote specs. Vim-parallel rules: the line goes to the first file, an explicit `file:line` beats `+N`, at most one `+N` is accepted, and `+N` with no file is an error with a hint. Out-of-range lines clamp exactly like `file:huge`. Bare `+` (vim: last line), `./+50`, and `+abc` remain ordinary filenames. The --help file-location section documents the new form (new `cli.file_syntax.plus_line` key translated in all 14 locales). Fixes #1926 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
Document the contract introduced for #2221: the ssh:// scheme is claimed exclusively by the remote parser, malformed URLs fail at startup, and ./ssh://... is the escape hatch for weird local names. Refs #2221 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
sinelaw
force-pushed
the
claude/fix-cli-args
branch
from
August 10, 2026 15:18
400109a to
05e2d39
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2221
Fixes #1926
Two CLI argument-parsing fixes, one commit each (plus a docs note).
Motivation
#2221 — ssh:// silently degrades to a local file. A userless
ssh://host/pathrun without$USER/$USERNAMEin the environment (and any other malformedssh://argument: missing path, bad port, empty user) fell back to opening a local buffer literally namedssh://.... No error, no SSH attempt — and saving that buffer offered to create a localssh://host/...directory, a data-loss trap for anyone following the documented URL form.#1926 — vim-style
+N.fresh +50 file.txtopened a stray empty buffer named+50, and in trailing position (fresh file.txt +50) that stray buffer also stole focus from the real file.The fix
ssh://:
parse_locationnow returns aResult, and thessh://scheme is claimed exclusively — anything starting withssh://that doesn't parse as a remote spec fails startup with the specific reason, e.g.:The
$USER/$USERNAMEfallback is injected as a parameter (parse_location_with_default_user) so tests cover the no-$USERcase without racing over the process environment. Local files with odd names (ssh:file,ssh:/x,./ssh://x) are untouched — only the exact scheme prefix is claimed. Note this intentionally changes the previous "malformed ssh:// is a local filename" tests: opening a local file that is literally namedssh://...now requires the./prefix (same escape hatch vim uses for+50-style names).+N:
+Narguments are folded into the file list once, right after clap parsing and before any consumer (TUI, GUI, web, attach, nested forward) sees it, by rewriting the target to the exact samefile:linesyntax path that already existed —fresh +50 file.txt==fresh file.txt:50, including remote specs (fresh +50 user@host:/path). Vim-parallel rules:file:lineon the target beats+N;+Nis accepted;+Nwith no file argument is an error with a hint;+0and beyond-EOF lines clamp exactly likefile:0/file:huge(verified:+99999on a 100-line file lands on the last line);+(vim: "last line") is not claimed — it has nofile:lineequivalent — and stays an ordinary filename, as do./+50and+abc. A real file named+50is still openable as./+50(breaking change relative to bare+50, matching vim's behavior).--helpdocuments the new form in the file-location syntax section; the newcli.file_syntax.plus_linekey is translated in all 14 locales.Tests
New unit tests (all in
crates/fresh-editor/src/main.rs):$USER; missing path / bad port / empty user → errors (these previously asserted the silent local fallback — assertions flipped to the correct behavior, so they fail without the fix); userless + injected default user works, incl. port and:line:col; scp-style unaffected by missing$USER; weird local names (ssh:file.txt,ssh:/host/path,sshfile,./ssh://host/path) stay local.+N: leading/trailing/multi-file placement,+0, saturation of absurdly large numbers, explicitfile:lineprecedence, remote specs, stdin-skipping, bare+/./+50/+abcuntouched, no-file and multiple-+Nerrors.Ran:
cargo test -p fresh-editor --bin fresh(70 passed),cargo test -p fresh-editor --lib i18n(locale parity),cargo check -p fresh-editor --all-targets,cargo check -p fresh-editor --all-targets --features gui,cargo fmt,cargo clippy -p fresh-editor(no new warnings).Manual validation (tmux, debug build, isolated
$HOME):env -u USER -u USERNAME fresh --no-restore ssh://localhost/etc/hosts→ clean startup error (above), exit 1, no editor, no local buffer.fresh --no-restore ssh://root@localhost/etc/hosts→ still printsConnecting via SSH to root@localhost...(control).fresh +50 test.txtandfresh test.txt +50→ singletest.txttab, status barLn 50, Col 1, no+50buffer, no focus steal.fresh +0 test.txt→Ln 1;fresh +99999 test.txt→ last line.fresh +50→ clear error with hint;fresh ./+50opens the literal file.fresh --helpshows+10 file.txt Open at line 10 (vim-style).Notes for review
RUST_BACKTRACE=1(force-set inreal_main), so a backtrace follows the message — same as existing startup errors (e.g. mixed local/remote); not new to this PR.deferredlabel: the issue thread contains no argument against the syntax — the owner's only comment was a clarifying question ("from CLI, in addition to filename:line?", answered yes) — so this implements it fully; happy to trim to just the stray-buffer fix if the deferral was intentional.🤖 Generated with Claude Code
https://claude.ai/code/session_01D6SkGXXTcJsytTjF1TBf8Y
Generated by Claude Code