8af44dfcd2
A fast, read-only ~/.ssh/config picker that exec's the system ssh client. Includes: static high-contrast picker with recency/frequency/name sort and tag grouping, `dial keygen` (native ed25519/RSA + optional config entry), and an optional offline YubiKey launch gate with one-time recovery codes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
132 lines
6.9 KiB
Markdown
132 lines
6.9 KiB
Markdown
# Project Brief: Interactive SSH Host Picker CLI ("dial")
|
|
|
|
## Overview
|
|
A fast, interactive command-line tool that reads `~/.ssh/config`, lets the user
|
|
fuzzy-select a host, and `exec`s the system `ssh` binary into it. Cross-platform
|
|
(macOS Apple Silicon/M4, Linux, Windows). Optional YubiKey gate before the app
|
|
will run. App-specific data lives inside the `.ssh` tree so it rides along on
|
|
existing Syncthing sync across machines.
|
|
|
|
## Goals
|
|
- Parse `~/.ssh/config`, including `Include` directives.
|
|
- Present an interactive, fuzzy-searchable list of `Host` entries.
|
|
- On selection, `exec` the real `ssh` binary (not a wrapped subprocess) so the
|
|
terminal/tty, `ProxyJump`, agent forwarding, etc. all just work.
|
|
- Cold start fast enough to feel instant (target: comfortably under 100ms).
|
|
- Ship as a single static binary per platform — no runtime dependency.
|
|
- Optional YubiKey challenge-response gate required before the picker unlocks.
|
|
- Store app data (recents, favorites, tags, settings) under `~/.ssh/` so it
|
|
syncs automatically via the existing Syncthing setup — no new sync config
|
|
needed.
|
|
|
|
## Core Features
|
|
1. **Config parsing**
|
|
- Read `~/.ssh/config`, follow `Include` lines.
|
|
- Surface `Hostname`, `User`, `Port`, `ProxyJump` as preview info.
|
|
- Optional lightweight tagging via comment convention, e.g.
|
|
`# tag: prod, riedel` above a `Host` block — used for filtering/grouping.
|
|
- Never write to `~/.ssh/config` — strictly read-only.
|
|
|
|
2. **Interactive picker**
|
|
- **Static, numbered list** — not a live fuzzy-filter-as-you-type interface.
|
|
Explicitly avoid the cramped/fuzzy-filter style seen in tools like the
|
|
`rclone` installer, which is hard to read in low light. Prioritize
|
|
legibility: larger clear text, generous spacing between entries, high
|
|
contrast, no small dense text packed together.
|
|
- Navigate with arrow keys or number keys, enter to connect, esc to quit.
|
|
- Optional `/` to jump into a search/filter mode on demand (not the
|
|
default view) if the host list gets long — but the resting state should
|
|
always be a clean, static, readable list.
|
|
- Sort by most-recently-used or most-frequently-used, toggleable.
|
|
- Show a small preview area (Hostname/User/Port/Jump host) for the
|
|
highlighted entry, in equally readable, high-contrast text.
|
|
|
|
3. **Connect**
|
|
- `exec` (or platform equivalent) into `ssh <alias>` so control of the
|
|
terminal passes cleanly to the ssh session — this app should not remain
|
|
a parent process wrapping it.
|
|
|
|
4. **YubiKey gate (optional, toggle in settings)**
|
|
- HMAC-SHA1 challenge-response (YubiKey slot 2), fully offline — no
|
|
internet or server dependency.
|
|
- Implemented by shelling out to `ykman` (Yubico's official CLI) rather
|
|
than a direct PC/SC library. Trade-off accepted: `ykman` (and its
|
|
Python dependency) must be installed separately on every machine this
|
|
runs on — this breaks the "single static binary, zero deps" goal for
|
|
the YubiKey feature specifically, but keeps the implementation simple
|
|
and leans on Yubico's own well-tested tool. `dial` should check for
|
|
`ykman` on `PATH` at startup and give a clear error if it's missing.
|
|
- Require physical touch to confirm.
|
|
- **Backup 2FA code**: generate a one-time recovery code when the gate is
|
|
first enabled, shown once and expected to be stored by the user
|
|
somewhere separate (password manager, printed, etc.). This code lets
|
|
the user in once if the YubiKey isn't available, avoiding total
|
|
lockout. After use it should be invalidated/regenerated, not reusable
|
|
indefinitely.
|
|
|
|
5. **Sync-safe app data**
|
|
- New directory: `~/.ssh/.dial/`
|
|
- `settings.json` — yubikey on/off + slot, theme, sort preference, etc.
|
|
- `data.json` — recents, favorites, tag cache.
|
|
- Multiple machines may write this concurrently via Syncthing — design for
|
|
that: last-write-wins is acceptable for v1, but reads should tolerate a
|
|
mid-sync/partial file without crashing (e.g. read failure just falls
|
|
back to defaults rather than erroring out).
|
|
- Nothing secret is ever written here — the YubiKey secret never leaves
|
|
the key itself in challenge-response mode, so there's no key material to
|
|
accidentally sync.
|
|
|
|
## Non-Functional Requirements
|
|
- No telemetry, no network calls (beyond ssh itself).
|
|
- Read-only with respect to `~/.ssh/config`.
|
|
- Graceful degradation if no YubiKey is plugged in and the gate is enabled.
|
|
- Works identically on macOS (Apple Silicon), Linux, and Windows — including
|
|
Windows' different `ssh.exe` path/behavior.
|
|
- **Readable in low light**: default view is a static list with clear,
|
|
well-spaced, high-contrast text — not a dense fuzzy-search UI. This is a
|
|
hard requirement, not a nice-to-have.
|
|
|
|
## Recommended Tech Stack
|
|
**Go** — chosen over your usual Python/Bash default specifically because this
|
|
needs true single-binary cross-platform distribution and fast cold start;
|
|
Python's interpreter startup and packaging story (PyInstaller etc.) are a
|
|
worse fit here.
|
|
|
|
- TUI: `charmbracelet/bubbletea` + `lipgloss` (or simpler: `charmbracelet/huh`
|
|
for a lighter list-picker if a full TUI is overkill).
|
|
- SSH config parsing: `kevinburke/ssh_config`.
|
|
- YubiKey challenge-response: shell out to `ykman` (simplest, but adds an
|
|
external dependency) or use a Go PC/SC library directly for a fully
|
|
self-contained binary — worth deciding based on how important "zero
|
|
external deps" is to you.
|
|
- Cross-compile via `GOOS`/`GOARCH` for the three targets from one machine.
|
|
|
|
*Alternative:* Rust would give similar performance/single-binary benefits
|
|
with a steeper setup cost — mention if you'd rather Claude Code default to
|
|
that instead.
|
|
|
|
## Out of Scope (v1)
|
|
- Editing/managing `~/.ssh/config` from within the tool.
|
|
- Remote host key (known_hosts) management.
|
|
- Any custom ssh-agent handling — rely on the OS-native ssh client already
|
|
installed.
|
|
|
|
## Future / v2 Considerations
|
|
Not in scope for v1, but the architecture should not preclude these — worth
|
|
keeping the "select a host" logic reusable/decoupled from the "connect via
|
|
ssh" action so these can be added later without a rewrite:
|
|
- **SFTP file transfer**: after selecting a host, an alternate action to
|
|
send/receive files instead of opening a shell session.
|
|
- Simplest version: shell out to the system `sftp`/`scp` binary, same
|
|
pattern as the `ssh` exec flow.
|
|
- Fuller version: an in-app file browser (source/dest picker, remote
|
|
directory listing, progress) built against a Go SFTP library
|
|
(e.g. `pkg/sftp`) rather than shelling out — should follow the same
|
|
readable, high-contrast, low-light-friendly list UI as the host picker,
|
|
not the cramped style of a bare `sftp` prompt.
|
|
|
|
## Open Decisions (Claude Code should confirm before building)
|
|
- YubiKey gate: required every launch, or only after an idle timeout?
|
|
- Exact format/length of the backup recovery code, and where it should be
|
|
displayed/stored on generation (e.g. printed to terminal only, once).
|