termuna

SSH and SFTP

SSH is built in, not bolted on: hosts live in an encrypted vault, connections support jump hosts and port forwards, and every SSH session carries its files with it.

Adding a host

Open the SSH connections screen (the drawer, or Ctrl+K and type ssh) and add a host. Choose how to authenticate: your SSH agent, a password, or a key file. Add jump hosts and port forwards on the same profile if you need them. A host that authenticates with your agent can also forward it, off by default and switched on per host; what that sends, what it risks, and how Termuna narrows it are on Using the terminal.

Saved hosts are sealed in a local vault, encrypted at rest with a machine-local key. Nothing about a host leaves your machine unless you turn on connection sync.

Agents and keys that never touch disk

A host that authenticates through an agent never needs its private key in Termuna: not in the vault, not in a file on disk. Termuna asks the agent for a signature and nothing else, so 1Password, gpg-agent, the OpenSSH agent and a hardware key behind any of them all work the same way. Each host says which agent it uses, one of three values: inherit the environment's socket (SSH_AUTH_SOCK), an explicit socket path, or none, which disables the agent for that host.

The add-host form has a Test button. It asks the agent what it holds and lists the answer: the type and comment of every identity, with a mark on hardware-backed keys that need a touch before they sign. The test waits four seconds at most, so a dead socket is a clear answer rather than a hang.

When authentication fails, the pane prints which socket was tried, where that socket was named (the environment, the host's own setting, or ~/.ssh/config), every identity the agent offered, and one sentence on what to do next. The four cases are on Troubleshooting.

On Windows, a host with no socket set talks to the OpenSSH agent over its named pipe, \\.\pipe\openssh-ssh-agent. If the pipe is missing, start the ssh-agent service. Forwarding the agent to the host is a separate switch, off by default: Agent forwarding.

Importing existing hosts

Hosts in ~/.ssh/config are not imported automatically - the connections screen offers an explicit import instead, so you decide what enters the vault. IdentityAgent is read along with the host, with ~, ${VAR} and the OpenSSH percent tokens expanded the way OpenSSH expands them, so a host that names its own agent socket keeps it, and IdentityAgent none keeps the agent off for that host. A linked host shows the result on its row: agent · <socket>, or agent off · IdentityAgent none.

One connection per host, many panes

Termuna opens one SSH connection per resolved target: the user, the host, the port, the jump chain and the identity that actually authenticated. Every pane on that target, and the file lane behind the SFTP browser, is a channel inside it. A split on a host you are already connected to costs a channel open, not a handshake, so it appears at once, and a host that asks a 2FA or keyboard-interactive question asks it once, for the connection, not once per pane. The pane that paid for the connection says so in its own scrollback before the dial; the panes that join afterwards print nothing.

A question the server asks at sign-in (a Duo passcode, a token code, an expiring password: keyboard-interactive) is asked of the connection, so it is asked once, and the daemon holds it rather than the window that dialled. That is why it can be answered from your phone or from the dashboard's Machines view as well as at the desktop card: the round appears with the server's own text quoted verbatim, one field per prompt, masked exactly when the server said to mask it, and a countdown, and whichever surface answers first holds it while the others say who has it. The answer travels sealed with the machine's own key, is opened by that machine and by nothing in between, and is kept nowhere.

The connection counts its consumers. When the last one closes, the link is not torn down at once: it lingers for connection_linger, five minutes by default, and a new pane or file browse inside that window reuses it. "0" closes it with the last consumer. Sharing is on by default (share_connections = true) and can be turned off for one host alone with [ssh.host."<name>"] share_connections = false; the keys are in the config reference.

A pane gets a connection of its own when sharing is off for its host, or when the server refuses a second channel (MaxSessions 1, say). It then wears an amber own connection badge carrying the server's own stated reason, never an invented one, and a refusal is remembered for the rest of the daemon's life so the host is not probed again on every split. A host that forwards your agent always gets its own connection.

A drop is one event, not one per pane. Every pane, file browse and port forward on the connection is marked lost in the same moment, and exactly one redial runs for all of them, on the schedule below; a split into a host that is already being dialed again joins that redial rather than starting a second one. On success every pane gets a fresh channel, and exhausted attempts or a changed host key leave every consumer with the same named reason.

To see what the daemon holds, open Ctrl+K > Connections: every connection with its host, identity, state, channel count, who is using it and its uptime. Closing a row closes the connection and tells its consumers; it never kills a pane. A pane that joined a connection this machine already held wears a brief mint shared · N ms badge with the daemon's own measurement of that open, next to the few hundred milliseconds a fresh handshake costs.

When the link drops

A dropped SSH link does not close the pane. The pane keeps its scrollback and its place in the split, and Termuna dials the same target again with the credentials the profile already holds: nothing asks you for a secret a second time. On a shared connection that is one redial for every pane on it, never one per pane. The waits between attempts are capped and short, one second, two, four, eight, then fifteen for every attempt after that, up to redial_max attempts, eight by default. Both keys are in the config reference; redial = false turns redial off entirely.

The break is written into the pane's own scrollback as a seam, so the transcript says where the connection was lost and what came back. What comes back is a new shell: a redial restores the link, not the remote state. Whatever was running on the far side ended with the old connection. Run tmux or screen on the server when you need the remote side to survive the outage too.

Two cases are never retried, on purpose. A target that authenticates with an interactive password is not dialed again, because there is no way back to it without asking you for the secret. A target whose host key changed is not dialed again either: accepting a new key is your decision, not a background one. Both leave the pane inert and say which one it is, and typing into a pane that is not live is dropped rather than queued, so the shell that comes back never receives half a command. What each state means is on Troubleshooting.

Land on the server with your own setup

A host can carry a kit: your rc files, your environment variables and Termuna's own terminfo entry, sealed in the same vault as the host itself and applied on the far side before your shell starts. You get your own setup on a machine you have never logged into, without copying dotfiles around or editing anything on the server.

It is removed when the session ends, and applied again if the link drops and dials again. The whole kit is capped at 64 KiB, and a host with no kit starts exactly as it does today. Your setup, carried, in full.

Files over SFTP, and past it

On any SSH session opened from the vault, the files button opens an SFTP browser for that host: browse, upload, and download without leaving the session. That is the start of the file story, not the end of it. The same directory shows beside the live terminal in the web dashboard and on the phone, and a file dropped on a pane rides the shell's own bytes to wherever the pane reaches, jump hosts and containers included, where SFTP cannot follow. All of it is on Moving files.

Vaults

Connections organize into named vaults - Personal, Production, one per client. Locally they group your hosts; with a paid plan they sync across your machines, and on a Team plan a vault can be shared with teammates, wrapped to each member's own key (how that works).

When the machine is yours, skip the jump host

SSH needs a path into the target, which is what bastions and jump chains are for. For machines you control, there is a simpler shape: install the headless daemon and the box dials out to the relay instead - no inbound port, no jump host, reachable from your phone even behind NAT or a VPN. From a session there, SSH onward to anything the machine can see.

Next: Moving files