Initial commit: dial — interactive SSH host picker

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>
This commit is contained in:
2026-07-22 23:18:23 +10:00
commit 8af44dfcd2
31 changed files with 4219 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
# 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).