# 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`).