Files
dial/REPORT.md
T
bsncubed 8af44dfcd2 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>
2026-07-22 23:18:23 +10:00

153 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dial — project report
A status report on the `dial` CLI, written to be self-contained so it can be
pasted into a fresh chat for brainstorming. No access to the codebase is needed
to follow it.
## What dial is
`dial` is a fast, interactive command-line SSH host picker. It reads
`~/.ssh/config` (strictly read-only), shows a clean static list of hosts, and
on selection `exec`s the real system `ssh` client into the chosen host — so the
terminal/tty, `ProxyJump`, agent forwarding, etc. all "just work" because dial
gets out of the way (it does not stay a parent process wrapping ssh).
It's written in **Go**, ships as a **single static binary** per platform, and
targets macOS (Apple Silicon + Intel), Linux (amd64 + arm64), and Windows.
## Design decisions already made
These are settled and built:
- **Language: Go.** Chosen over Python/Bash for true single-binary
cross-platform distribution and fast cold start.
- **UI style: static, numbered, high-contrast list — NOT fuzzy-filter-as-you-
type.** This was a hard requirement: legible in low light, generous spacing,
large clear text, high contrast. A `/` key opens an on-demand search/filter,
but the resting state is always the clean static list.
- **TUI library:** `charmbracelet/bubbletea` + `lipgloss`.
- **Config parsing:** hand-written parser (not a library) so it can follow
`Include` directives *and* support a custom `# tag: prod, riedel` comment
convention above `Host` blocks for grouping/filtering.
- **Connect = process handover.** Unix uses `syscall.Exec` (replaces the
process); Windows runs ssh.exe as a child with inherited stdio and exits with
its status.
- **App data lives in `~/.ssh/.dial/`** so it rides along on the user's existing
Syncthing sync — no new sync config. Two files: `settings.json` (prefs +
hashed gate data) and `data.json` (recents, frequency, favourites). Writes
are atomic; reads tolerate a partial/corrupt file mid-sync by falling back to
defaults; concurrent cross-machine writes are last-write-wins.
- **YubiKey gate (optional):** offline HMAC-SHA1 challenge-response by shelling
out to Yubico's `ykman` CLI (slot 2 default). This is the one feature that
breaks "zero external deps" — `ykman` must be installed separately — a
deliberate trade to lean on Yubico's own tool. dial checks for `ykman` on
PATH and errors clearly if missing.
- **Gate timing: required on every launch** (chosen over an idle-timeout).
- **Backup recovery code:** 6 groups of 5 base32 chars (~150 bits), shown in
the terminal exactly once at enable time, stored only as a salted hash.
Single-use: after it's used to get in, it's invalidated and a fresh one is
issued (shown once).
- **Nothing secret is ever stored or synced.** In challenge-response mode the
YubiKey secret never leaves the key; dial persists only a random (non-
secret) challenge plus a salted hash of the expected response.
- **Selection is decoupled from the connect action** so a future SFTP mode can
reuse the picker without a rewrite. There's already a stubbed `SFTP` command
alongside `SSH`.
## Architecture (package layout)
- `cmd/dial` — entrypoint + CLI subcommands (`yubikey enable/disable/status/
recovery`, `version`, `help`) and the pre-TUI gate flow.
- `internal/sshconf` — read-only config parser: Include following, tag comments,
extracts Hostname/User/Port/ProxyJump.
- `internal/store` — the `~/.ssh/.dial/` persistence (atomic, sync-tolerant).
- `internal/picker` — the Bubbletea TUI (static list, on-demand filter, sort
toggle, favourites, preview pane).
- `internal/connect` — the exec-into-ssh handover (per-OS files).
- `internal/yubikey` — ykman challenge-response + recovery code logic.
- `internal/theme` — high-contrast, low-light-friendly styling (adapts to
light/dark terminals).
## What the picker does today
- Static numbered list; navigate with arrows or `j`/`k`; `1``9` jump to a host.
- `enter` connects (exec ssh); `q`/`esc` quits.
- `/` opens an on-demand substring filter over alias/hostname/user/tags; `esc`
returns to the static list.
- `s` toggles sort between **recently used** and **most used** (persisted).
- `f` toggles a favourite; favourites pin to the top regardless of sort.
- A preview box shows the highlighted host's Host/HostName/User/Port/ProxyJump.
- Tags from `# tag:` comments show inline and are searchable.
## Current state
- **Complete and building.** All five platform binaries cross-compile
(~3.5 MB each). Native build via `make build`; all binaries via `make dist`.
- **Tested:** unit tests for the parser (includes/tags/edge cases), the store
(round-trip, corrupt-file fallback, favourites), the yubikey logic (enroll/
verify with a simulated key, recovery-code round-trip), and picker rendering/
overflow. `go vet` clean.
- **Performance:** parsing a 200-host config is ~0.3 ms; cold start <10 ms —
well under the "comfortably under 100 ms" target.
- **Live-verified via tmux:** navigation, on-demand filtering, favourite +
sort persistence to disk.
## Known limitations / not yet exercised
- **The YubiKey gate has never touched real hardware** — `ykman` isn't installed
on the dev box, so that path is only covered by unit tests with a simulated
HMAC key. Needs a real touch-test on a machine with a key. The exact `ykman`
subcommand (`ykman otp calculate SLOT CHALLENGE`) assumes ykman 5.x; older
versions used different command names.
- **No signing/notarization.** The macOS/Windows binaries are unsigned, so
Gatekeeper/SmartScreen will warn unless the user clears quarantine or builds
locally.
- **Not in version control** yet (no git repo initialised).
- **No release automation** (no CI, no GitHub releases, no Homebrew tap/scoop).
- **Match blocks** in ssh_config are recognised but their conditions aren't
evaluated (dial just doesn't attach stray keywords to a host). Wildcard
`Host *` patterns are excluded from the connectable list.
## Explicitly out of scope for v1
- Editing/managing `~/.ssh/config` from within the tool (strictly read-only).
- `known_hosts` management.
- Custom ssh-agent handling (relies on the OS ssh client).
## Open questions & directions to brainstorm
From the brief's "future/v2" plus things that came up:
1. **SFTP / file transfer mode.** After selecting a host, an alternate action to
send/receive files. Simplest: shell out to system `sftp`/`scp` (same exec
pattern). Fuller: an in-app file browser (remote listing, progress) built on
a Go SFTP library, following the same readable low-light list UI. The
picker is already decoupled to allow this.
2. **How to pick the action** (shell vs SFTP) once a host is selected — a
second keypress on the highlighted host? a mode toggle? a menu?
3. **Distribution/trust.** Worth signing/notarizing? A Homebrew tap + Scoop
manifest + an `install.sh`? GitHub Releases via CI?
4. **Gate ergonomics.** Every-launch touch is secure but can nag. Is an
opt-in idle-timeout worth adding later? Should the gate also cover the
future SFTP action?
5. **Recovery-code UX.** Currently terminal-only, shown once, single-use with
auto-rotation. Is that the right balance vs. something like multiple codes,
or a printable sheet?
6. **Richer metadata.** Tags exist; would grouping/sectioning by tag, or a tag
filter shortcut, be useful? Colour-coding by environment (prod/staging)?
7. **Search behaviour.** Filter is currently plain substring (predictable, not
fuzzy) to honour the "readable, not cramped" ethos. Is fuzzy matching wanted
as an option?
8. **Multi-select / batch actions** (e.g. run a command across several hosts) —
in scope for a tool like this, or scope creep?
9. **Connection history/analytics** beyond recents/frequency — last-connected
timestamps shown in the list? per-host notes?
## How to run/install (for reference)
- Build: `make build` (native) or `make dist` (all platforms).
- Install a binary onto PATH, e.g. Linux user-level:
`install -D -m 0755 dial-linux-amd64 ~/.local/bin/dial`.
- Use: `dial` (needs a `~/.ssh/config`; there's an `examples/ssh_config.sample`).
- Gate: `dial yubikey enable` (needs `ykman`).