Files
dial/README.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

8.4 KiB
Raw Permalink Blame History

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/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:

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