Files
bsncubed 0ecdffbbbd docs: correct Go version and remove unshipped SFTP browser claims
- README: build-from-source requires Go 1.25+ (matches go.mod), was 1.23+.
- docs/v2-report.md: the in-app SFTP file browser was never built (no `t`
  action, no xfer/sshx/browser packages, pkg/sftp not a dependency). Cut the
  false "shipped" claims and moved it into the Deferred section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 16:27:59 +10:00

234 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 <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
```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).