Oryxis 0.15.0
The mosh release, and the first since 0.10 to carry a security section. Switch mosh on for a host and the shell rides out sleep, a change of Wi-Fi and a change of address, with the interface saying how long the link has been out of touch rather than pretending it is fine. Alongside it, a batch of fixes that mostly share one shape: a value somebody else authored being trusted as if you had typed it, plus three that stop a remote peer deciding how many bytes land on your disk. The last closed tab comes back, the curated terminal themes reach 31, and a key sitting in ~/.ssh is finally a key Oryxis will offer.
mosh
- An option on an SSH host, not a seventh protocol. It has to be:
mosh-serverdoes not exist until an SSH session starts it, and the port and key it answers with come back over that same channel. So a mosh host is an SSH host, with the username, the key, the jump chain, the proxy and the host-key policy the SSH side already resolves. Four fields in the host editor under Credentials: the toggle, a server path, a port range for hosts behind a firewall that opens only a few, and a command that replaces the login shell (which is not the connection's startup command: that one is typed at a shell once it is up, and this one is what survives a disconnect). - The settings are kept when the toggle goes off. They arm nothing on their own, and a server path somebody had to look up should not have to be found again.
- The handover sits where every dial path converges, so the whole SSH connect experience is reused as it stands, prompts and host keys and proxy consent and all, and a dial site added later inherits it rather than growing its own half. The SSH connection is then closed on the way out, which is mosh's own behaviour and is not a saving: SSH is TCP, it dies the moment the address changes, and keeping it would give the tab two lifetimes and let half of it break exactly when the other half proved its worth.
- The link says how long it has been out of touch. A mosh session stays alive while its network does not, which is the whole point of the protocol and the one thing "connected" cannot express. Two clocks, mosh's own, because the link fails in two ways and naming the wrong one sends you debugging in the wrong direction: nothing arriving at all is no contact, things arriving with nothing acknowledged is no reply, which is what a one-way path looks like. Amber on the tab strip and in the connection segment, with the direction and the duration in the latency slot.
- The screen model lives on alacritty, the same emulator the pane draws with, rather than a second terminal in the binary holding a second opinion about one screen. What that costs is a differ, written here and tested by round trip: apply what it produced to the screen it was computed from and the result has to equal the screen it was computed against, cell by cell, cursor included.
- Asking for Files on a mosh pane opens an SFTP tab of its own. A session that survives roaming has no SSH connection to multiplex on, because the one that started it was let go precisely for not surviving either. Two tabs with two visible lifetimes is the honest shape, and it used to be a request that vanished without a word.
- Requires
mosh-serveron the host and a reachable UDP port. A host behind a jump chain is the one case that cannot work: SSH arrives there from the last hop, so the server binds an address facing the bastion, and UDP does not travel down an SSH tunnel. Upstream mosh has the same limitation for the same reason. - Expect the login banner to be missing on most mosh sessions. Ubuntu prints it from two places with different rules:
pam_motd, in sshd's own PAM stack, fires on every SSH login, and mosh's shell is started bymosh-serverrather than by sshd, so that one never runs for it. What is left is/etc/profile.d/update-motd.sh, which stamps~/.motd_shownand then stays quiet for the rest of the day, so the first mosh session of the day gets a banner and the rest get none. Upstream mosh behaves identically.
Security
- Opening a URL on Windows went through
cmd.exe, which parsed it.cmd /C start "" <url>put a shell between Oryxis and the browser, and Rust quotes an argument only when it holds a space or a tab. A URL holds neither, so&,|and^reachedcmd.exeas operators and%VAR%expanded: a link readinghttps://host/?a=1&calcrancalcon the click. Both callers reach that sink with a string somebody else authored, one of them being whatever the remote host printed into your terminal, the other the updater's release metadata, which a download mirror is free to author. NowShellExecuteWwith no shell in between, plus an RFC 3986 scheme allowlist at each sink, because the Windows handler resolves a bare path or a UNC name to a program and runs it. - A command proxy now asks this machine before it runs on this machine.
ProxyType::Commandis the one connection field that becomes a local process, handed tosh -cbefore the handshake, so it runs regardless of host-key policy, reachability or a failed auth. And that data does not always come from the person at the keyboard: a paired sync device could point an existing group at a planted command and getsh -c <attacker>on the next dial of any host in it, including the auto-start forward that fires at boot with nobody present. The records still replicate, because replication is not the bug. What changed is that the spawn asks: the gate is in the engine rather than its ten callers, so a dial site that never heard of it fails closed; approval is keyed by the command's SHA-256 and never leaves the device, because an approval that synced would decide for someone who never saw the line; unattended dials may only look up an existing approval, never raise a dialog nobody expected; and only a line typed into the host editor is pre-approved, because a picked file is not a read file. - A sync peer may add a host-key pin, not swap one. A pin is a decision a human made at a fingerprint prompt on this device, and storage keeps one row per host, port and key type, deleting the others first. So a peer's record carrying a fresh entity id was never an insert: it replaced the local fingerprint, and every later dial to that host trusted the peer's key with no prompt, headless strict dials included. It also slipped both existing gates, which are keyed by entity id and therefore read a forged record as brand new. What replaces automatic propagation of a key rotation is the ordinary "changed" prompt on the next connect, which is the one moment a human should be looking at a fingerprint anyway.
- An import file is not a source of trust. The import dialog shows a per-category count and never a list, so nothing on that path gives you a chance to see what a file changes. Settings whose value is itself a trust decision now stay behind:
download_mirrorre-points every GitHub-bound request including the updater's, a clipboard access level ofreadwritehands every connected server an OSC 52 clipboard read, and the agent-server four activate the local signing service and strip its per-signature confirmation. Host-key pins dedup by the semantic key so an import cannot replace one, and port-forward rules land disarmed, becauseauto_startis what turns a stored rule into a dial at the next launch with nobody present. - A downloaded folder owes the same name rule as its children. The recursive SFTP walk already refused unsafe server-supplied names per entry, which left the two places that name the destination themselves. Both build a local path from a name the server chose, and splitting on
/only neutralizes/, so a backslash, a..or aC:prefix survived it and re-rooted the join, writing outside the directory you picked. The multi-row arm skips a bad name so one hostile entry cannot sink the rows you also selected; the single-file and root arms fail, because there the bad name is the request. - A ZMODEM batch is bounded, not just each file in it. A download starts from six bytes the server printed, and the sender picks both the sizes and how many files follow. The only ceiling reset at every file header, so a peer looping ZFILE, ZDATA, ZEOF stayed inside it forever while the disk filled. There is a session budget now, computed from free space.
- A download asks whether it fits before it starts. Three paths let a remote peer decide how many bytes land on your disk and none of them asked. SFTP answers before the first byte moves rather than failing at 90% with a stranded part file. Best-effort by construction: a platform that will not answer returns nothing and the caller proceeds, because a check that cannot run must never be the reason a transfer refuses.
- A recording that fills the disk stops, and says so. Session recording wrote for as long as the peer kept printing, with no size in hand at all, and a full volume ended in a log line and nothing else: recording kept being attempted, every later byte was dropped, and you were never told. There is an unconditional free-space floor now (not a setting, because nobody switches on "do not fill my disk", and the vault sits on the home volume), and a stop both toasts and flags the row truncated, because a partial stream presenting itself as a whole session is a worse failure than stopping.
Tabs, themes and keys
- Bring back the last closed tab (#186). Ctrl+Shift+Y, the tab context menu and the command palette reopen the last tab that left the strip, terminal or SFTP, ten deep and session-only. It resolves through the same machinery a dormant pin reopens through, so a saved host comes back by id and survives renaming and reordering, a local shell by its program and args, and an SFTP tab as a dormant chip that re-mounts on the first select. Quick-connect tabs are deliberately not remembered, for the reason they are not pinnable: their credentials would have to outlive the session that typed them.
- The tab's close X waits out a hover dwell. On a session chip it takes the host badge's slot, so revealing it the instant the pointer arrives puts a destructive target exactly where the cursor already is, and switching tabs quickly with the mouse closed sessions instead of selecting them. It now needs 300 ms of rest, a hover-episode counter retires the timer of the chip the cursor left, and closing several tabs in a row suspends the dwell because there the strip, not the cursor, is what moves. The active chip is unchanged: its X is already there.
- Fourteen more built-in terminal themes, taking the curated set to 31: Ayu Dark and Light, Catppuccin Mocha and Latte, Everforest Dark, GitHub Dark and Light, Gruvbox Light, Horizon, Kanagawa, One Light, Rosé Pine, Tokyo Night, Zenburn. Each is faithful to its upstream terminal export with the divergences documented rather than smoothed over: Catppuccin and Rosé Pine repeat normal slots into the brights on purpose, Gruvbox Light's brights are the darker faded row, and Horizon has no white brighter than its foreground. At 31 the lists needed a filter, so the Settings gallery and the per-host picker each got one.
- A key in
~/.sshis now a key Oryxis can offer. The third key source, next to the vault and the agent, opt-in per host and never a global setting, because offering a credential you never named changes who you are to that server. The vault always wins: the disk only fills a slotkey_idor a linked identity left empty, and only for the methods that offer a key at all. A passphrase-protected file is reported as exactly that in the host editor rather than silently skipped, which is what used to leave you watchingsshauthenticate while Oryxis fell through to a password prompt with nothing on screen saying why.
Protocols and the editor
- One protocol picker (#174). Remote desktop used to be a separate entry in the add menu, which is exactly how a feature becomes invisible: someone looking for RDP opened the protocol picker, did not find it, and concluded the app had none. Every protocol now lives in one list, joined by Raw (a bare TCP line for console servers, which opens in silence because a console server forwards what it receives down the serial line and an unasked-for option burst lands on the attached device as garbage), Local (a curated terminal on this machine, referenced rather than copied, so a synced host still resolves) and Telnet over TLS.
- Quick connect speaks the same list. A typed
telnet://,raw://,serial://ortelnets://decides the protocol, and a bare/dev/ttyUSB0orCOM3is Serial because it is a host under no protocol. The scheme always wins over the card's badges: the text is what you wrote. - The dial prompts answer the keyboard. The host-key and command-proxy approvals record keyboard rows now, with the refusing button as the default, so a stray Enter can never trust a host key or spawn a command proxy. Before this the keyboard could only ever refuse: reaching "continue" needed the mouse, at exactly the moment your hands are not on it.
- A scheme pasted into the wrong import panel is carried over rather than dead-ending on an error in a language the panel could not even show properly (discussion #68). The content and the typed name move to the panel that handles them, the section switches and a toast says what happened; apply there still runs every validation. The redirect needs positive evidence of the other kind, never mere absence of the marker, so a typo cannot ping-pong between the two panels.
Files and transfers
- A single-file download gets the progress bar the upload already had. "Download to local" on one file ran with no bar, no byte count and no cancel, which on a 900 MB file is a UI that looks frozen. Both directions share one batch runner now, and a file picked at top level carries its size so the bar advances by bytes rather than sitting at 0 of 1 for the whole run.
- Drop uploads ask before overwriting (#185, by @shideqin). Dropping onto the terminal or the sidebar routes through the same conflict flow the SFTP panel uses (replace, replace if different, duplicate, cancel, optionally sticky for the rest of the drop) instead of silently renaming or clobbering, and answering resumes the upload from where it stopped.
- Drag files out onto the desktop, phase two. The ghost is drawn by Oryxis and the OS only takes over when the cursor leaves the window, because the Windows drag API blocks the UI thread for the whole gesture: escalating at the movement threshold froze the window before a ghost could paint once, and inside our own window the OS has nothing to show anyway. Resolving the payload at the threshold also keeps the round trip off the critical path, so a quick flick out finds it ready.
- Secrets-free CSV export of hosts (Settings > Security), round-tripping through the importer. There is structurally no password column: the function never receives secret material at all, and the encrypted portable export stays the only secrets-bearing path.
- A session-recording size cap (Settings > Security), which drops the oldest finished recordings the way any log rotation does. It also fixes an asymmetry the age rule has alone, where "1 day" deletes a 10 KB recording from yesterday and keeps a 40 GB one from today.
Fixes worth naming
- A jump host that itself sits behind a jump chain is now reached (#184). The route was built from the final host's chain alone, so C via B, with B behind A, never got to B. OpenSSH follows a hop's own
ProxyJumprecursively and so does this, under a visited guard so a cycle degrades to a direct dial rather than looping. - Pasting works again on Wayland and WSLg (#179). A fallback added for compositors without the clipboard-control protocol fixed writing and took reading with it: WSLg answers with an empty string for content that reads correctly over X11, and an empty string is not an error, so it arrived as a legitimately empty clipboard.
- Inline IME preedit at the caret (#178, by @shideqin, for #176), so composing in Japanese, Chinese or Korean shows what is being composed instead of nothing.
- The classic Alt+Tab switcher shows the app icon on Windows (#182) instead of a generic placeholder.
- Keystrokes no longer land in the wrong tab while another is connecting (#177, by @shideqin).
- Drag and drop of files into the window, via synchronized fixes in the winit and iced forks (#183, by @shideqin).
- Portable Windows copies and Linux AppImages update in place (#180). A portable copy used to be handed the setup installer, which lays down a second, installed copy instead of updating the one you run; an AppImage now replaces the image file
$APPIMAGEpoints at rather thancurrent_exe, which is the read-only mounted squashfs. Both keep the Ed25519 signature gate. - The AI assistant knows when a command has actually finished. The prompt was detected by finding a
$,#or%anywhere in a line, socating a script ending in# end of fileread as "the shell is back", and adfrow ending in41% /run/userwas the same trap one column over. Where the cursor sits is what discriminates a prompt from a line that merely contains a marker, and that is the signal now; behind that gate the marker test can afford to be loose enough for fish and oh-my-zsh prompts, which never ended a capture at all before. - An open side panel no longer eats the search box's Enter (#175).
- An identity that no host links to said
???rather than saying so, in every one of the 23 languages: the string reached the lookup with no table entry behind it. - The transfer toasts lost their protocol name: they say the same thing for a ZMODEM transfer, an SFTP queue item and a drag-out, and naming one of the three made the other two read as borrowed strings.
Under the hood
- New crate,
oryxis-mosh: the handover (remote command synthesis with shell quoting, announcement parsing, both pure and testable without a server) and the session driving themosh-rscrate, publishing escape bytes on the same channel shape Telnet and Serial use, so nothing downstream learns a new transport. - The paste path leaves a trace, and never the content. A paste that does nothing looked the same in a log as a paste that never happened; there is now one debug line per stage of the funnel (the gesture asking for the clipboard, what came back, which gate the text hit, the bytes handed to the session), covering the selection half too, and shape only, because a clipboard carries whatever was copied last.
vt100is gone from the build entirely, and there is onealacritty_terminalin the workspace rather than two emulators disagreeing about one screen.- Every transport now reads as dead before its output stream ends. The end of that stream is what tells the app a session disconnected, and the app discards such a notice while the pane's transport is still alive, because the mosh handover makes a superseded session ordinary rather than exotic. That test is only safe if a session that really died can never still answer "alive" at that instant, so each reader publishes its own death before dropping the output sender, in the same task. SSH leaned on the reader task's join handle and Telnet / Serial on a channel the writer task closes, both of which settle a scheduling decision later than the notice travels. Nobody hit it and no test could reproduce it; the point is that the guard no longer depends on winning a race.
- The e2e job runs the suite twice before reporting red, with a warning line saying a retry happened. Two of those tests are timing-sensitive by nature and the runner aborts on the first failure, so one flake used to lose the whole suite and hide every file after it alphabetically.
- 2051 tests across 52 binaries,
clippyclean on--all-targets.
What's Changed
Full Changelog: v0.14.0...v0.15.0