Architecture
sshelf is a single binary that runs in one of two modes:
- Interactive TUI (default, and subcommands like
import) — the fuzzy launcher. - Askpass helper (
SSHELF_ASKPASS=1in the environment) — a headless, non-interactive mode thatsshinvokes to obtain a stored password. Never run directly by the user.
High-level flow
┌─────────────────────────────────────────────┐
│ sshelf (TUI) │
│ │
hosts.toml ──▶│ store ──▶ model(Host) ──▶ search (fuzzy + │
state.json ──▶│ frecency) ──▶ ui (list/wizard) │
config.toml ─▶│ │
│ user presses Enter on a host ──┐ │
└────────────────────────────────────┼──────────┘
│
1. update frecency (use_count, last_used) & save
2. set env: SSH_ASKPASS=self, SSH_ASKPASS_REQUIRE=force,
SSHELF_ASKPASS=1, SSHELF_HOST_ID=<id>
3. tear down TUI (raw mode off, leave alt screen, show cursor)
4. exec("ssh", argv…) ← process is REPLACED; sshelf is gone
│
▼
┌─────────────────────────────────────────────┐
│ ssh │
│ needs a password? ─▶ runs SSH_ASKPASS: │
│ `sshelf "<prompt>"` (SSHELF_ASKPASS=1) │
│ │ │
│ ▼ │
│ askpass mode: inspect argv[1]; │
│ if password prompt ─▶ secrets.get(host_id) │
│ (keyring → age vault) ─▶ print, exit 0 │
│ else ─▶ exit non-zero (decline) │
└─────────────────────────────────────────────┘
│
interactive ssh session
│
session ends → back at shell
Why this shape
-
exec()(process replacement), not spawn+wait. The user chose exit-to-shell: when the SSH session ends, they’re back at their normal prompt.exec()givessshthe real TTY with zero indirection — the cleanest possible handoff. Consequence: no code runs afterexec(), so anything that must persist (frecency) happens before it. -
Password auto-supply via
SSH_ASKPASS, notsshpass.sshnever accepts a password on the command line;sshpasswould expose it inps/argv and is an extra dependency.SSH_ASKPASS(OpenSSH 8.4+) letssshcall a helper program for the password. We point it at our own binary. WithSSH_ASKPASS_REQUIRE=force, ssh uses the helper even though a TTY is present. Seessh-command.mdfor the full mechanism and its sharp edges (the helper must inspect the prompt; host-key prompts must be neutralized). -
Two-tier secrets. OS keyring (macOS Keychain / Linux Secret Service) is the primary store. Headless/minimal Linux often has no Secret Service daemon, so an
age-encrypted vault (unlocked by a master passphrase, cached in-memory per session) is the fallback. Seesecurity.md. -
Own database, never
~/.ssh/config. Hosts live inhosts.toml(human-readable, atomic writes). Import from~/.ssh/configis read-only. Seedata-model.md. -
Synchronous event loop, component pattern. No background work needs multiplexing (the one long-running thing, the SSH session, happens after the TUI is gone). A simple
crossterm::event::read()loop with a component-per-screen structure (HostList, Wizard, Help, Confirm) keeps it small and tokio-free.
Component map
See structure.md for the file-by-file breakdown. At runtime:
Appowns top-level state (current screen, query, selection, loaded hosts + state) and routes events to the active component.searchturns(hosts, state, query)into a ranked, highlight-annotated view.sshis the only place that builds argv and performs the teardown +exec().secretsis the only place that talks to the keyring/vault; both the TUI (to store on add/edit) and the askpass mode (to retrieve) go through it.
Failure handling
- If
exec()returns, it failed (e.g.sshnot found) → restore the TUI and surface the error. - If the askpass helper can’t get the secret → exit non-zero so
sshfalls back to prompting the user, rather than hanging or sending a wrong answer. - A panic mid-TUI restores the terminal via a guard + panic hook before unwinding.