Force-add the cross-platform binaries (macOS arm64/amd64, Linux amd64/arm64, Windows amd64) and their SHA256SUMS into dist/, rebuilt from current source with `make dist` (go1.26.5, -trimpath). All tests pass. These override the /dist/ .gitignore entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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
execs 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 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/configentry. - 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:
# 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:
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:
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):
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:
$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.23+.
make install # builds and installs into $GOBIN (or $GOPATH/bin, ~/go/bin)
Make sure that bin dir is on PATH:
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 CLI |
Usage
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 <host>) |
/ |
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
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
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.
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
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).