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