Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

UX: screens, keys, wizard, theming

Visual model is atuin.sh: slim chrome, an inline filter-as-you-type list, and a contextual keybind hint bar at the bottom.

Main screen (host list)

┌ sshelf  3/14 ───────────────────────────────────────┐
│ > prod                                               │   ← live fuzzy filter input (top)
└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ ▸ prod-web       deploy@web1:2222       [prod]       │   ← selected row, matched chars bold
│   prod-db        mike@10.25.25.25       [prod,db]    │
│   prod-cache     mike@10.0.0.9          [prod]       │
└──────────────────────────────────────────────────────┘
 ↵ connect  ^a add  ^e edit  ^d delete  ^y yank  ^o import  F1 help  esc quit

Layout: Length(3) search · Min(0) list · Length(1) hint bar. Each row shows name · user@host[:port] · [tags]. The matched/total count lives in the search-box title (so it’s never truncated by a narrow terminal).

Sorting / ranking

  • No query (idle): sort by frecency desc (most-used-recently first). score = use_count * exp(-decay_rate * days_since_last_used), decay_rate default 0.2.
  • Typing a query: fuzzy-filter via nucleo-matcher; sort by match score; frecency breaks ties. Matched characters are highlighted (bold/accent) using the matcher’s match indices, rendered with unicode-width so wide/combining chars don’t misalign.
  • v1 ships fuzzy search only (prefix/substring modes can come later).

Keybindings (list screen)

The search box is always active (atuin-style single mode), so plain letters filter the list. Actions therefore use Ctrl (or function keys) to avoid being typed into the query.

KeyAction
typefilter the list (fuzzy)
tag:NAMEfilter by tag; combine with text and repeat (tag:prod tag:db, AND)
site:NAMEfilter by site (one site per host)
/ , Ctrl-p / Ctrl-nmove selection
Enterconnect to selected host (tears down TUI, exec()s ssh) — M3
Ctrl-aadd host (wizard) — M4
Ctrl-eedit selected host — M4
Ctrl-ddelete selected host (confirm) — M4
Ctrl-yyank — copy/print the generated ssh command without connecting — M3
Ctrl-topen the dual-pane file-transfer screen for the selected host
Ctrl-fopen the port-forward popup for the selected host (runs in the background)
Ctrl-oimport from ~/.ssh/config (read-only) — M7
F1help overlay
F2settings (config & hosts-file locations)
F3manage sites (create/edit/delete groups + their shared defaults)
F4manage port forwards (list all active, stop any)
Escclear the query if non-empty, otherwise quit
Ctrl-cquit

Implemented in M2: type-to-filter, navigation, Enter (returns a Connect outcome), F1 help, Esc/Ctrl-c. The action keys show a “coming in Mx” status until their milestone. Tag filtering and config.toml keybinding overrides land in M6.

Add / Edit form

A single full-screen form (Ctrl-a add, Ctrl-e edit selected). Every field shows a dim placeholder explaining it. The form is auth-aware — it shows only the fields relevant to the chosen Auth method, so the rest don’t clutter the screen.

Always shown: Name (required), Hostname (required), User (defaults $USER), Port (defaults 22), Auth, Jump hosts, Tags, Site, 2FA (/ yes/no — prompt for a verification code on connect), Extra args (raw flags escape hatch).

Auth-specific fields:

AuthExtra fields
agentnone
keyKey/ cycles private keys found in ~/.ssh, Enter opens a file browser to pick a key anywhere (e.g. a .pem in ~/Downloads); Key passphrase — optional, only if the key is encrypted
passwordPassword

Key discovery finds keypairs (<name>.pub sibling) and standalone private keys including .pem (detected by a PRIVATE KEY header), so AWS-style keys show up too. Every field shows a dim placeholder, prefixed optional · when the field can be left blank (required · for Name/Hostname).

File browser (opened from the Key field with Enter): a modal listing the current directory with a fuzzy filter — type to filter, / move, Enter opens a directory or selects a file, goes up, Backspace edits the filter (or goes up when empty), Esc clears the filter (or cancels when empty). It starts in ~/.ssh (or near the current key) and a picked path is stored as the host’s identity, even if it’s outside ~/.ssh. Key discovery finds .pem and other private keys by their header, not just .pub pairs.

Settings (F2)

A screen for configuring sshelf itself. v1:

  • Config file — shown read-only (it’s chosen before the config is read, via --config / $SSHELF_CONFIG, so it can’t be a setting in the file itself).
  • Hosts file — editable; blank means the default under the config dir. ~ is expanded. On save, an existing file at the new path is adopted (loaded, never overwritten); a new path is created from the current hosts so they follow. More settings will be added here.

Navigation: Tab / / move between fields; / (or space) change the choosers (Auth, Key); Enter advances and saves on the last field; Ctrl-s saves anywhere; Esc cancels. Validation errors (missing name/hostname, non-numeric port) show inline and focus jumps to the offending field.

Implemented as a single-screen, auth-aware field form (not a paged wizard) — simpler to navigate/edit and “guided” by placeholders + inline validation. The Key field is a picker (single key); a host configured with multiple identity files keeps them on edit, but entering several keys is done by editing hosts.toml.

Quick-add: the form opens with defaults, so typing a Name + Hostname and Ctrl-s is enough.

Secrets (Password / Key passphrase): the masked value is stored in the OS keyring (or the age vault) keyed by host id — never in hosts.toml. On edit, leaving it blank keeps the existing secret. It’s auto-supplied at connect time (see ssh-command.md). Deleting a host (Ctrl-d) removes the host, its frecency entry, and its stored secret.

Sites (F3)

A site groups hosts (one per host — a data center / project) and, unlike tags, may carry optional shared SSH defaults — user, port, ProxyJump (bastion), identity — that member hosts inherit at connect time. A bare site (name only) is pure grouping; per-host fields always override the site’s; auth stays per-host.

  • In the list: with an empty search box, hosts are grouped under ── site (n) ── headers (a (no site) group last); while filtering, the list is flat with a dim ·site· column and a site:NAME filter token.
  • Assigning a site: the add/edit form has a Site chooser (/ over the defined sites
    • (none)).
  • Managing sites (F3): a list with a add, e/Enter edit, d delete, Ctrl-s save, Esc cancel. Each site’s form is name + optional user/port/jump/identity. Renaming a site updates its member hosts; deleting one clears members’ site (nothing dangles). A host that names an undefined site still groups under that name but inherits nothing.

Inherited defaults appear in the generated command (yank, print-command, connect, transfers).

Import (Ctrl-o / sshelf import)

Parses ~/.ssh/config read-only via ssh2-config and adds every host whose name isn’t already present, warning about unsupported Match / Include / ProxyJump. v1 imports all new hosts at once (no per-host selection screen) — curate afterwards with Ctrl-e / Ctrl-d. The CLI form supports --dry-run to preview. Never writes back to ~/.ssh/config.

File transfer (Ctrl-t)

Ctrl-t on a host opens a dual-pane transfer screen: local files on the left, the host’s files on the right. sshelf authenticates once by opening an ssh ControlMaster that reuses the host’s auth (keys/agent/ProxyJump, or the stored keyring/vault secret via SSH_ASKPASS), then runs sftp (ls/get/put) over it. Remote listing and transfers run on a background thread, so the UI stays responsive on slow links.

Both panes fuzzy-filter as you type:

KeyAction
typefilter the focused pane
Tabswitch the focused pane (local ↔ remote)
/ , Ctrl-p / Ctrl-nmove the selection
/ Enteropen the selected directory (or send a file)
Ctrl-ssend the selected file or folder (recursive) into the other pane’s directory
go up a directory
Backspaceedit the filter, or go up when it’s empty
Esccancel a running transfer, else clear the filter, else close the screen

A progress bar shows bytes and percent for single-file downloads; folder and upload transfers show as in-flight (cancelable with Esc). Directories are marked name/ and symlinks name@; symlinks are skipped in this version. Filenames are shell-quoted and control characters stripped from display. The connection uses StrictHostKeyChecking=accept-new, like connect — so a first-time host key is trusted on use (see security.md). Only one transfer runs at a time in v1, and a same-named file or folder already present in the destination is skipped (with a message) rather than overwritten.

On failure the status line shows the underlying sftp error. For more detail, run with sshelf --transfer-log <FILE> (or $SSHELF_TRANSFER_LOG=<FILE>): the worker appends every ssh/sftp command and its full stderr to that file. The log holds no secrets — the password reaches ssh via SSH_ASKPASS, never the command line.

Port forwarding (Ctrl-f / F4)

Ctrl-f on a host opens a small popup to start an SSH port forward that keeps running in the background even after you quit sshelf. Pick a kind (cycle with ←/→):

  • Local (-L, the default) — open a local port that tunnels to a host reachable from the server (e.g. expose a remote DB at 127.0.0.1:8080).
  • Remote (-R) — open a port on the server that tunnels back to a host reachable from here.
  • Dynamic (-D) — a local SOCKS proxy that routes through the server.

Fill in the ports/host (defaults: bind 127.0.0.1, target host localhost) and Ctrl-s to start. sshelf spawns a detached ssh -N …, reusing the host’s auth exactly as connect does (keys/agent/ProxyJump or the stored password via SSH_ASKPASS, plus any site defaults), and waits briefly to confirm it bound — a failure (port already in use, a privileged <1024 port, the server refusing a remote bind, or an auth failure) is shown in the popup so you can fix a field and retry. On success the TUI returns to the list and the tunnel runs on its own.

F4 opens the forwards manager: every active forward across all hosts, with its host, an L/R/D summary (L 127.0.0.1:8080 → db:3306), pid and age. d (or k) → y stops the selected one. The list refreshes live, so a forward that ends — stopped here, killed from another terminal, or dropped on its own (sleep / network) — disappears within a moment. Forwards survive sshelf exiting (each is a detached process in its own process group) and the ledger (forwards.json) is reconciled against the running processes on every launch, so you only ever see forwards that are still actually running. See decisions.md D-021.

Two-factor (2FA) hosts

Some servers want an interactive verification code (TOTP / keyboard-interactive) on top of your key or password. Mark such a host with 2FA = yes in the add/edit form (or sshelf add … --2fa). On connect, sshelf shows a small popup to enter the current code before handing off to ssh, and supplies it to the verification prompt through the same askpass helper that supplies your stored password — sshelf never proxies the live session.

The flag is needed because a connect that auto-supplies a stored secret runs ssh with SSH_ASKPASS_REQUIRE=force, which routes the code prompt to the helper with no terminal fallback — so without it the connect would simply fail. (A host with no stored secret already prompts for the code inline after handoff, so the flag mainly matters for stored-secret hosts; a host using an encrypted key with no stored passphrase should use an agent.) sshelf <host> from the CLI (no TUI) prompts for the code on the terminal instead. v1 is manual entry — sshelf does not store TOTP seeds. See decisions.md D-022.

CLI (outside the TUI)

CommandWhat it does
sshelfLaunch the interactive TUI.
sshelf <host>Connect straight to a saved host by name or id, skipping the TUI — same connect path as Enter (frecency recorded before the exec, secret auto-supplied). A miss suggests close names; a name that collides with a subcommand (list, import, …) is reached via the TUI instead.
sshelf -Reconnect to the most-recently-used host (max last_used in the frecency state). Errors (without connecting) if there’s no history yet.
sshelf addWith no args, opens the TUI add form. With args, adds a host non-interactively: NAME + -H/--hostname required; -u/--user, -p/--port, -a/--auth, -i/--identity (repeatable, implies key auth), -J/--jump, -t/--tag, -s/--site, --extra "<flags>", --password-stdin (reads the secret from stdin). Duplicate names are refused.
sshelf sites [--json]List defined sites with member counts + their shared defaults.
sshelf sites add NAME [-u/-p/-J/-i]Define a site (settings optional; edit later with F3).
sshelf print-command <host>Print the generated, shell-quoted ssh … command for a saved host by name or id (reflecting any inherited site defaults), without connecting or changing frecency. CLI equivalent of the TUI’s Ctrl-y yank.
sshelf list [query]List hosts (with a ·site· column). query filters with the TUI’s syntax — fuzzy text and/or tag:NAME / site:NAME (e.g. sshelf list site:prod-dc).
sshelf list --json [query]Same selection, emitted as JSON (each host’s fields + its generated command). Always valid JSON, even when empty — the stable surface for scripts/integrations.
sshelf import [--dry-run]Read-only import from ~/.ssh/config.
sshelf set-password <host>Store a password (read from stdin) for a host.
sshelf completions <shell> · sshelf manEmit static completions / the man page.
--config FILE (global)Use a specific config file (also $SSHELF_CONFIG).
--transfer-log FILE (global)Append transfer-screen diagnostics — the ssh/sftp commands and their errors (no secrets) — to FILE. Also $SSHELF_TRANSFER_LOG.

Dynamic completion (host names): static completions cover subcommands/flags only. Sourcing COMPLETE=<shell> sshelf (e.g. source <(COMPLETE=zsh sshelf)) enables host-name completion via clap_complete’s engine — host_name_candidates reads hosts.toml (side-effect-free) and is attached to the <host> arguments of direct-connect, print-command, and set-password.

Confirmations & overlays

  • Delete pops a confirm modal (y = delete, any other key = cancel).
  • Help (F1) is an overlay listing all keys; any key closes it.

Theming

atuin-inspired defaults: dim chrome, a single accent color for selection + match highlights. Terminal resize is handled automatically by ratatui’s layout — no manual recompute.

Configuration (config.toml)

A commented default is written on first run. Keys:

KeyDefaultMeaning
decay_rate0.2Frecency decay per day (higher = recency matters more).
default_sort"frecency"Idle list order: "frecency" or "name".
accent"cyan"Accent color: black/red/green/yellow/blue/magenta/cyan/white/gray.

Location: ~/.config/sshelf/config.toml (honors XDG_CONFIG_HOME).