# dial A fast, interactive SSH host picker. `dial` reads your `~/.ssh/config` (strictly read-only), shows a clean, static, high-contrast list of hosts, and `exec`s the real `ssh` client into the one you pick — so your tty, `ProxyJump`, agent forwarding, and everything else just work. - **Single static binary** — the picker and key generation are self-contained (see [Requirements](#requirements) for the few things that lean on the system `ssh`/`ykman`). - **Cross-platform**: macOS (Apple Silicon + Intel), Linux (amd64 + arm64), Windows. - **Fast cold start** — parsing a 200-host config takes well under a millisecond. - **Readable in low light**: the resting view is a static, numbered, generously spaced list. No cramped fuzzy-filter-as-you-type UI. - **Sort & group** by recency, frequency, or name; group by tag with coloured tag pills; pin favourites. - **Key generation** (`dial keygen`) — native ed25519/RSA keys with an optional matching `~/.ssh/config` entry. - **Optional YubiKey gate** with an offline HMAC-SHA1 challenge-response and a one-time backup recovery code. - **Sync-safe app data** stored under `~/.ssh/.dial/` so it rides along on your existing Syncthing sync — no new sync config needed. ## Install `dial` is a single binary. "Installing" just means putting it in a directory that's on your `PATH` and making it executable. Grab a prebuilt binary from `dist/` (`make dist`) or build one with `make build`. Pick the binary for your machine: | Platform | Binary | | ------------------------ | ------------------------- | | macOS Apple Silicon (M-series) | `dial-darwin-arm64` | | macOS Intel | `dial-darwin-amd64` | | Linux x86-64 | `dial-linux-amd64` | | Linux ARM64 (e.g. Pi, servers) | `dial-linux-arm64` | | Windows x86-64 | `dial-windows-amd64.exe` | ### Linux Put it in a directory that's on `PATH`. `install -D` copies the file, sets the executable bit, and creates the destination directory if it's missing — so a single line works either way. Two common choices: ```sh # System-wide (all users) — needs sudo, always on PATH: sudo install -D -m 0755 dial-linux-amd64 /usr/local/bin/dial # Just you (no sudo): install -D -m 0755 dial-linux-amd64 ~/.local/bin/dial ``` `~/.local/bin` is on `PATH` by default on most modern distros. If `dial` isn't found after installing there, add it: ```sh echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc exec $SHELL # reload your shell ``` Verify: `dial version`. ### macOS Apple Silicon uses `dial-darwin-arm64`; Intel Macs use `dial-darwin-amd64`. macOS ships the BSD `install`, which has no `-D` flag, so create the directory first: ```sh sudo mkdir -p /usr/local/bin sudo install -m 0755 dial-darwin-arm64 /usr/local/bin/dial ``` `/usr/local/bin` is on `PATH` by default. Because the binary isn't code-signed, Gatekeeper may quarantine it if it arrived via a browser or AirDrop — clear that once (harmless if it wasn't quarantined): ```sh xattr -d com.apple.quarantine /usr/local/bin/dial 2>/dev/null dial version ``` (Binaries copied over `scp`/`rsync` or built locally are not quarantined.) For the optional YubiKey gate: `brew install ykman`. ### Windows Put `dial-windows-amd64.exe` somewhere permanent as `dial.exe` and add that folder to `PATH`. In PowerShell: ```powershell $dir = "$env:LOCALAPPDATA\Programs\dial" New-Item -ItemType Directory -Force -Path $dir | Out-Null Copy-Item dial-windows-amd64.exe "$dir\dial.exe" # Add to your user PATH (persists across sessions): [Environment]::SetEnvironmentVariable( "Path", [Environment]::GetEnvironmentVariable("Path","User") + ";$dir", "User") ``` Open a new terminal, then `dial version`. Windows uses the OpenSSH `ssh.exe` that ships in `%WINDIR%\System32\OpenSSH`. ### Build from source instead Requires Go 1.25+. ```sh make install # builds and installs into $GOBIN (or $GOPATH/bin, ~/go/bin) ``` Make sure that bin dir is on `PATH`: ```sh echo 'export PATH="$(go env GOPATH)/bin:$PATH"' >> ~/.bashrc # or ~/.zshrc ``` Or just `make build` and copy `bin/dial` wherever you like (see the per-OS steps above). ## Requirements The binary itself has no dependencies, but individual features lean on tools you already have: | Feature | Needs | | ---------------------- | ----------------------------------------------------------- | | Picker + connect | the system `ssh` client (already installed) | | Key generation | nothing — keys are generated natively | | YubiKey gate | Yubico's [`ykman`](https://developers.yubico.com/yubikey-manager/) CLI | ## Usage ```sh dial # pick a host and connect ``` In the picker: | Key | Action | | ---------------- | ------------------------------------------------- | | `↑`/`↓`, `j`/`k` | move the selection | | `1`–`9` | jump to that numbered host | | `enter` | connect (exec `ssh `) | | `/` | open the on-demand search/filter | | `s` | cycle sort: recently used → most used → name | | `g` | toggle grouping by tag | | `f` | toggle favourite (favourites sort first) | | `q` / `esc` | quit | The list resting state is always the clean static view. Search is opt-in via `/`; `esc` returns you to the static list. ### Generating keys ```sh dial keygen # ed25519 by default; --rsa for RSA-4096 ``` Prompts for a key name, an optional passphrase, and (optionally) a matching `Host` entry to add to `~/.ssh/config`. The private key is written `0600`, the `.pub` is kept, and any config append backs up the file first and refuses to duplicate an existing alias. dial never overwrites an existing key. ### Tags Add a comment above a `Host` block to tag it. Tags show in the list and are searchable: ``` # tag: prod, riedel Host web1 HostName 10.0.0.5 User deploy ``` ### Includes `Include` directives are followed (globs and `~` are expanded, relative paths resolve against `~/.ssh`), so hosts spread across multiple files all appear. ## YubiKey gate (optional) When enabled, `dial` requires a YubiKey touch on every launch before the picker unlocks. It uses slot 2 by default, fully offline. This feature shells out to Yubico's official [`ykman`](https://developers.yubico.com/yubikey-manager/) CLI, which must be installed separately (this is the one part that isn't a zero-dependency single binary — a deliberate trade to lean on Yubico's own tooling). `dial` checks for `ykman` on `PATH` and reports a clear error if it's missing. ```sh dial yubikey enable # enroll your key (touch it) + print a one-time backup code dial yubikey status # show current gate state dial yubikey recovery # issue a fresh backup code (after unlocking) dial yubikey disable # turn the gate off ``` **Backup recovery code.** Enabling the gate prints a one-time recovery code **once** — store it somewhere separate (password manager, printed). It lets you in if the key isn't available. It is single-use: after you use it, a fresh code is issued and 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 challenge and a *salted hash* of the expected response; the recovery code is stored only as a salted hash. ## App data (`~/.ssh/.dial/`) - `settings.json` — gate on/off + slot, theme, sort preference, and the hashed recovery code / YubiKey verification record. - `data.json` — recents, usage frequency, favourites. Both are written atomically (temp file + rename). Reads tolerate a partial or corrupt file mid-sync by falling back to defaults, and concurrent writes across machines are last-write-wins. ## Building releases ```sh make dist # cross-compiles all platforms into ./dist ``` ## Scope `dial` does no telemetry and makes no network calls beyond `ssh` to the hosts you pick. The picker treats `~/.ssh/config` as read-only; the only write dial ever makes is `dial keygen` adding a `Host` block (with a backup).