Interactive CLI/TUI to switch SSH keys, inspect the agent, view hosts, and edit SSH config.
Status: through v0.7.0 (milestones 0–30, 39, 40, 42, 43) — full read/merge
pipeline, agent switch (load/unload/unload-all), host directive + key/host CRUD
with backup+confirm, wildcard hosts, read-only Match blocks, restore-from-backup,
key↔host association, hot reload, app config with multiple default identities +
auto-load, configurable SSH dir/config path, multi-algorithm key generation,
scrollable panes, live search/filter, connect-to-host, permission audit+fix,
known_hosts management, clipboard copy, smart shell-init, help overlay, opt-in
motion system, 16 color themes (fg+bg, in-app switcher), launch update-check,
lipgloss styling, e2e suite + ubuntu/macOS CI, packaging (completions, man page,
install script, brew/AUR config), and a versioned self-update/release pipeline.
Adaptive two-column layout (M41) was tried and reverted — full-height single pane
reads better and avoids row truncation. Match-block editing is deferred
(surfaced read-only for now); brew/AUR/install-script publishing awaits public
releases + tap/AUR secrets. Next window: v0.9.0 (v1.0 stabilization) → v1.0.0.
Tests cover every pkg; see README.md for usage.
| Topic | Choice | Reason |
|---|---|---|
| Config write | Round-trip via github.com/kevinburke/ssh_config AST |
Preserve user comments, ordering, unknown options. Mutate nodes, not regenerate. |
| Agent read | golang.org/x/crypto/ssh/agent over $SSH_AUTH_SOCK |
List + fingerprint loaded keys, no external dep. |
| Agent write | Shell to ssh-add / ssh-add -d |
Terminal handles encrypted-key passphrase prompt natively. |
| Config scope | ~/.ssh/config + Include‘d files |
Covers real setups. Writes only to user files. |
| Key gen | Shell to ssh-keygen |
Don’t reimplement key generation. |
| App settings | TOML at ~/.config/sshush/config.toml (BurntSushi/toml) |
sshush’s own prefs (default identity), separate from ~/.ssh. |
| Hot reload | fsnotify watching config dirs + ~/.ssh, debounced |
Pick up external edits without a manual refresh. |
| Releases | goreleaser + GitHub Actions on v* tags |
Cross-platform binaries + checksums, reproducible. |
| Self-update | creativeprojects/go-selfupdate (GitHub releases) |
sshush update: detect, checksum-verify, replace in place. |
cmd/sshush/main.go entrypoint + subcommands (TUI, load-default, shell-init, update, version)
pkg/config/ domain model (Identity, Host, SshConfigModel)
pkg/sshconfig/ parse + round-trip write (kevinburke wrapper)
pkg/keys/ scan ~/.ssh for keypairs; generate/delete keys
pkg/agent/ agent client: List (Go proto), Add/Remove/RemoveAll (exec ssh-add)
pkg/service/ orchestrator: builds unified model, mediates mutations
pkg/appconfig/ sshush settings (default identity, SSH dir/config overrides) at ~/.config/sshush
pkg/watch/ fsnotify wrapper, debounced change signals (hot reload)
internal/tui/ BubbleTea views/update — thin, no IO of its own
TUI never touches files/agent directly. All IO behind pkg/service interfaces → testable headless, mockable, hot-reload-ready. (For interactive subprocesses — ssh-add on an encrypted key, ssh-keygen — the TUI uses tea.ExecProcess with command builders from pkg/agent/pkg/keys, the one sanctioned exception to “no direct IO”.)
// pkg/sshconfig — round-tripping config read/write
type ConfigRepo interface {
Load() (*config.SshConfigModel, error) // parse user config + Includes
SetHostField(h config.HostID, key, val string) error
DeleteHostField(h config.HostID, key string) error
AddHostIdentity(h config.HostID, path string) error // IdentityFile +=
RemoveHostIdentity(h config.HostID, id config.IdentityID) error
AddHost(config.Host) error
DeleteHost(config.HostID) error
Save() error // backup <path>.bak, then write AST
}
// pkg/keys — disk scan + key file management
type KeyScanner interface {
Scan() ([]config.Identity, error) // walk ~/.ssh, pair pub/priv
Generate(GenerateOpts) (config.Identity, error) // ssh-keygen
Delete(privPath string) error // remove priv + .pub
}
// pkg/agent — talk to ssh-agent
type AgentClient interface {
List() ([]AgentKey, error) // loaded keys + fingerprints
Add(path string) error // exec ssh-add <path> (inherits tty)
Remove(path string) error // exec ssh-add -d <path>
RemoveAll() error // exec ssh-add -D
}
// pkg/service — the only thing TUI sees
type Service interface {
Refresh() (*config.SshConfigModel, error) // scan+parse+agent, merged
AddKeyToAgent(config.IdentityID) error
RemoveKeyFromAgent(config.IdentityID) error
UnloadAllKeys() error
EditHost(h config.HostID, field, val string) error // edit + Save + Refresh
DeleteHostField(h config.HostID, field string) error
AttachKey(h config.HostID, id config.IdentityID) error // write IdentityFile
DetachKey(h config.HostID, id config.IdentityID) error
AddHost(config.Host) error
DeleteHost(config.HostID) error
GenerateKey(keys.GenerateOpts) (config.Identity, error)
DeleteKey(config.IdentityID) error // unloads from agent first
}
The TUI also depends on two optional narrow interfaces it can run without:
appSettings (default identity; backed by pkg/appconfig) and fileWatcher
(hot reload; backed by pkg/watch).
Service.Refresh():
keys.Scan() -> []Identity (ExistsOnDisk)
sshconfig.Load() -> Hosts + Identity refs
agent.List() -> []AgentKey
merge by fingerprint: set Identity.LoadedInAgent / AgentFingerprint
-> *SshConfigModel (single source of truth, immutable snapshot to TUI)
TUI holds a snapshot. Mutations dispatched as tea.Cmd (async goroutine) → call Service → return result tea.Msg → TUI re-renders. Service re-Refreshes after any mutation so state stays consistent.
<path>.bak once per session before its first mutation.sshush shell-init prints a snippet, user pastes.$SSH_AUTH_SOCK, unreadable key, malformed config, no settings/watcher → status message, keep running.Two places use reflection to reach unexported fields of kevinburke/ssh_config,
pinned to v1.6.0 and guarded by tests that fail loudly on a library bump:
KV’s rawValue so an edited value re-renders (setKVValue), preserving indentation/comments;Host’s implicit flag (isImplicitHost) to skip the parser’s synthetic global block.| # | Deliverable | Risk |
|---|---|---|
| 0 | Libs added, package scaffold, interfaces stubbed, tests compile | none |
| 1 | pkg/keys scan + unit test w/ fixtures |
none (read) |
| 2 | pkg/sshconfig parse + Includes + round-trip test |
none (read) |
| 3 | pkg/agent List + fingerprint match |
none (read) |
| 4 | pkg/service Refresh merges all three |
none (read) |
| 5 | TUI: read-only Keys + Hosts panes | none — first useful build |
| 6 | Agent add/remove (switch keys) + unload-all; auto-expiring status | low (reversible) |
| 7 | Edit/add/delete host directives, Save w/ backup+confirm | write — backup gated |
| 8 | Add (wizard: basic + custom options) / delete host, keygen, key delete | write/destructive — confirm gated |
| 9 | Wildcard host (Host *) add/edit/delete + per-host key association |
write — surfaces wildcard blocks |
| 10 | Hot reload (fsnotify), reconcile | medium |
| 11 | App config (~/.config/sshush), default identity | low |
| 12 | load-default subcommand + shell startup snippet generator (print only) |
low |
| 13 | lipgloss styling pass: bordered panes, grouped help, key↔host links | none |
| 14 | README + installation instructions | none (docs) |
| 15 | Self-updating binary via GitHub releases | medium |
| 🏷 | v0.1.0 — first tagged release: full TUI feature set + self-update | — |
| 16 | Configurable SSH dir / config path (override ~/.ssh defaults) |
low |
| 17 | Key generation with selectable algorithm (ed25519/rsa/ecdsa) + bits | low |
| 🏷 | v0.2.0 — configurable paths + multi-algorithm keygen | — |
Everything below gates v1.0.0: usability at scale, correctness on real-world
configs, safety nets, and distribution. Releases are tagged as features land so
users get value before 1.0; the API/config surface only freezes at the RC.
| # | Deliverable | Risk |
|---|---|---|
| 18 | Scrollable / paginated panes (viewport) for long key & host lists | low |
| 19 | Search / filter within Keys and Hosts panes | low |
| 20 | Connect to host — Enter on a host launches ssh <alias> |
low |
| 🏷 | v0.3.0 — scrolling, search, connect-to-host | — |
| 21 | Permissions audit + fix (~/.ssh, keys, config, authorized_keys modes) |
low |
| 22 | known_hosts management (view/search, remove stale/changed keys) |
low |
| 23 | Copy to clipboard (public key / fingerprint / ready ssh command) |
low |
| 🏷 | v0.4.0 — perms fix, known_hosts, clipboard, multi-default, smart shell-init | — |
| 24 | Help overlay (?) + NO_COLOR, narrow-terminal, non-TTY handling |
low |
| 25 | Styling / polish pass + opt-in motion system | low |
| 26 | Themes — palette config, in-app switcher, open-source presets, randomize/reset | low |
| 🏷 | v0.5.0 — help overlay + polish/motion + theming (M41 adaptive layout reverted) | — |
| 27 | Match block + broader directive support (read/display, edit-safe) — read-only display done; editing deferred |
medium |
| 28 | Restore-from-backup (undo last write) command/action — done (R + sshush restore) |
low |
| 🏷 | v0.6.0 — Match blocks read-only + restore-from-backup (Match editing deferred) | — |
| 29 | Integration tests (real agent/keygen e2e) + CI matrix (linux/macOS) — done (-tags e2e, ubuntu+macOS matrix) |
low |
| 30 | Packaging: Homebrew tap, AUR, shell completions, man page — config + assets done; tap/AUR publishing needs secrets | low |
| 🏷 | v0.7.0 — e2e/CI matrix + packaging (completions/man/brew/AUR) + install script + update-check | — |
| 31 | v1.0 stabilization: error-handling audit, config schema freeze, docs/screenshots, CHANGELOG — done (RC ready; demo GIF needs vhs docs/demo.tape) |
low |
| 🏷 | v0.9.0 — release candidate (feature-complete, schema frozen) | — |
| — | soak period: bug-fix-only patch releases (v0.9.x) from real-world use | — |
| 🏷 | v1.0.0 — stable release (tag + announce) | — |
Daily-friction SSH features that ship after 1.0; each lands as a clear v1.x
bump. The “why it hurts” column is the user pain sshush removes.
| # | Feature | Why it hurts today | Target |
|---|---|---|---|
| 32 | Install key on remote (ssh-copy-id) |
manual append to a remote authorized_keys |
v1.1.0 |
| 33 | Agent add with lifetime / confirm (ssh-add -t/-c) |
keys live forever in the agent; no per-use confirm | v1.1.0 |
| 34 | Change / add key passphrase (ssh-keygen -p) |
obscure syntax; unclear which keys are encrypted | v1.2.0 |
| 35 | Key hygiene warnings (weak / old / orphan) | weak (RSA<2048, DSA), aging, and orphaned keys pile up unnoticed | v1.2.0 |
| 36 | Fingerprint + randomart view (ssh-keygen -lv) |
verifying a server key by eye is fiddly | v1.3.0 |
| 37 | Connection / auth test (up/down/auth badge) | “will it even connect?” needs a manual attempt | v1.3.0 |
| 38 | Backup & restore (export/import keys + config) | moving to a new machine is manual and risky | v1.4.0 |
Connectivity actions (20, 32, 37) reuse the tea.ExecProcess terminal-handover
pattern already used for ssh-add/ssh-keygen.
Newer asks, with the release window they slot into. Numbered 39+; they fold into the existing windows above (not a separate phase).
| # | Feature | Why / note | Target |
|---|---|---|---|
| 39 | Multiple default identities | auto-load several keys on startup, not just one — teams juggle work+personal+deploy keys. default_identities = [...] in config.toml; s toggles membership; load-default loads all |
v0.4.0 |
| 40 | Smart shell-init |
detect whether the snippet is already in ~/.bashrc/~/.zshrc: nudge to add it when a default is set (only if absent), and warn on shell-init if a stub is already present (avoid duplicates) |
v0.4.0 |
| 41 | Adaptive layout | v0.5.0 (reverted) | |
| 42 | Auto update-check on launch | async, non-blocking: check latest release in a tea.Cmd, surface “update available → run sshush update” as a transient status. Off for dev builds; respects a check_updates = false setting — done |
v0.7.0 |
| 43 | curl \| bash install script |
host an install.sh (detect OS/arch → download the right release asset + checksum verify → drop sshush on PATH); one-line install in the README — done; needs public releases (repo is private, so assets are auth-gated) |
v0.7.0 |
| # | Feature | Note | Target |
|---|---|---|---|
| 44 | Loud / “make some noise” mode | arcade-style SFX (Pokémon/Mario energy) on load/switch/error/connect. Independent on/off toggle, but only active when motion ≠ off; intensity rides alongside motion. Audio via a small embedded-asset player (e.g. beep/oto); fully optional, silent by default |
v1.5.0 |
| 45 | Stripped build + sshush update --stripped |
a build tag that excludes the sound assets (smaller binary, Loud mode disabled); update --stripped fetches that variant. goreleaser produces both |
v1.5.0 |
| 46 | Slim the binary: replace the self-update dependency | measured: go-selfupdate adds ~3.7 MB (9.0 → 5.3 MB, ~41%) by pulling in three forge SDKs (go-github/v74 + GitLab client + Gitea SDK). sshush only ships to GitHub — swap to minio/selfupdate + a single stdlib GET api.github.com/.../releases/latest. Keeps sshush update; zips drop ~3.4 → ~2 MB. (-s -w already applied; UPX cut another ~60% but breaks macOS Gatekeeper, so linux/windows only) |
TBD |
Not gating any version; standalone projects that amplify the tool.
Candidate ideas beyond the numbered roadmap — pulled in as they prove valuable:
Host aliases, unreachable/overshadowed blocks,
invalid option values, IdentityFiles that don’t exist on disk.LocalForward/RemoteForward/DynamicForward,
and launch a forward as a background session.ssh-keygen -c to rename a key’s comment in place.-C) as of
v0.9.2; deferred post-1.0: KDF rounds (-a), output format (-o/PEM), and
passphrase handling surfaced as wizard steps rather than ssh-keygen’s prompts.ssh/scp/sftp invocation, or an
~/.ssh/config snippet, for sharing.~/.ssh sets
(work vs personal) building on the configurable SSH dir.Ctrl+K) across hosts + commands.Two related gaps in the host model:
hostFromAST currently drops any block whose patterns are
all wildcards (*, ?, !), so Host * defaults are invisible and uneditable.
Surface wildcard hosts in the model (flag them, e.g. Host.IsPattern), list them
in the Hosts pane, and allow add/edit/delete. Edits route through the same
SetHostField/DeleteHostField path; findHost must match pattern blocks too.
Risk: editing Host * changes defaults for every connection — confirm copy
should make that explicit.Host.Identities
already exists in the model (parsed from IdentityFile); add UI to attach/detach
a key to the selected host, writing IdentityFile <path> directives (supports
multiple). On the Keys pane, optionally show which hosts use each key.README.md covering: what sshush is, a screenshot/asciinema, feature list, install
options (go install, prebuilt binary, build from source), the shell-init snippet
for shell-startup loading, keybindings, and the ~/.config/sshush/config.toml
settings. Keep ARCHITECTURE.md as the design doc; README is the user-facing entry.
Ship versioned releases and let the binary update itself:
-ldflags -X main.version=...; add a sshush
version subcommand. Tag releases vX.Y.Z.sshush update checks the latest release via the GitHub API,
downloads the matching asset, verifies a checksum, and replaces the running
binary in place (e.g. minio/selfupdate or creativeprojects/go-selfupdate).
Risk: replacing the executable + signature/checksum verification — gate clearly,
no auto-update without consent.Let users point sshush at a non-default SSH directory / config file. The
constructors already accept overrides (keys.New(dir), sshconfig.New(path),
agent.New(sock)); today cmd/sshush always passes "" (defaults), so nothing
is user-settable except the agent socket via $SSH_AUTH_SOCK.
ssh_dir and config_path to
~/.config/sshush/config.toml (appconfig). Optionally also accept
SSHUSH_SSH_DIR / SSHUSH_CONFIG env vars or CLI flags; precedence
flag > env > config file > default.cmd/sshush reads them and passes to keys.New /
sshconfig.New; the TUI’s hot-reload rewatch must watch the configured dir
instead of hardcoded ~/.ssh.resolveInclude currently resolves relative
Include globs and ~ against ~/.ssh; make it honor the configured SSH
directory so a relocated config’s includes still resolve. Likewise the
IdentityFile→IdentityID basename mapping is dir-agnostic and is fine.
Risk: read paths only; no new destructive surface.The new-key flow currently hardcodes ed25519. The backend already supports more:
keys.GenerateOpts carries Algorithm and Bits, and keygenArgs emits
-t <algo> (+ -b <bits> for non-ed25519). Only the TUI needs to expose it.
Algorithm/Bits into
keys.GenerateCommand (interactive, via tea.ExecProcess).
Risk: none beyond existing keygen.Lists currently render every row; long key/host sets overflow the pane box.
Add a viewport per pane (bubbles/viewport or manual windowing): track a scroll
offset, keep the cursor visible, page with pgup/pgdn, show a scroll
indicator. m.height already plumbs through.
A /-activated filter that narrows the active pane to matching rows
(substring/fuzzy on name, host alias, hostname, comment). Filtering is a view
concern over the existing sorted slices; esc clears. Pairs with M18 scrolling.
sshush manages SSH but can’t yet do SSH. Make Enter on a host launch
ssh <alias> via tea.ExecProcess — the TUI yields the terminal, the session
runs, and sshush resumes when it exits. Same terminal-handover pattern as
ssh-add/ssh-keygen. Use the host’s alias so the user’s own config (incl.
ProxyJump, etc.) applies. Optional: a key to copy the equivalent command.
SSH silently refuses keys and config files with loose permissions, producing
cryptic auth failures. Audit modes on ~/.ssh (700), private keys (600), config
and authorized_keys; surface offenders with a warning badge and offer a
one-key chmod fix (confirm-gated). Read-only detection is safe; the fix is the
only write and is reversible in spirit (tightening perms).
The REMOTE HOST IDENTIFICATION HAS CHANGED wall blocks logins and editing
known_hosts by hand is error-prone. Parse ~/.ssh/known_hosts (+ known_hosts2,
hashed entries), list/search entries, show the associated host, and remove a
stale/changed entry via ssh-keygen -R <host> (confirm-gated). Read-first, then
the targeted removal.
Pasting the right public key into GitHub/servers means hunting for the .pub.
Add clipboard actions (via an OSC 52 escape or a clipboard lib): copy a key’s
public key, its SHA256 fingerprint, or a ready-to-run ssh <user>@<host> -p <port>
-i <path> command for the selected host. Degrade gracefully where no clipboard
is available (print to status / stdout).
? opens a full keybinding reference (the grouped footer is
a summary; this is the complete list per mode).NO_COLOR; degrade on narrow terminals
(truncate columns, hide non-essential tags) and when stdout is not a TTY.A second styling pass once the surface is feature-complete (scrolling, search, connect, help all in): tighten spacing/padding and column alignment, refine color relationships and contrast, polish overlay/card framing, and unify glyph/iconography. Pure presentation — no behavior change. Lands before theming (M26) so the default palette it tunes becomes the baseline themes override.
Plus an opt-in motion system — playful, arcade-style juice:
[motion] setting in config.toml (and an in-app
toggle) enables it; enabled = false keeps the UI completely static. Always
forced off when NO_COLOR/non-TTY, or when the terminal can’t keep up.subtle / normal / arcade) scale amplitude,
frequency, and which effects run, so users dial it from a faint shimmer to full
retro chaos.●); animated transitions
for status and pane switches.tea.Tick, ~30–60ms)
that’s only scheduled while an effect is active (no idle CPU burn); represent
effects as small time-bounded state (start time, duration, kind) layered over
the existing render — never blocking input or writes. Reuse this same frame
loop, not a second one.Risk: presentation only, but guard performance (cap active effects, stop the ticker when idle) and respect the off switch unconditionally.
Status — core done; flavor deferred. Shipped: [motion] config + in-app
m toggle + intensity (subtle/normal/arcade), 16ms frame ticker scheduled only
while an effect plays (no idle CPU), palette-matched full-width flash bar on
success/error, and a punchy screen-shake (ease-out² decay + ~38Hz osc;
amp 2/4/7) on errors / destructive deletes / all arcade actions / the toggle demo.
Also the textStyle pass so panes don’t inherit the terminal foreground.
Deferred (continuous effects — conflict with the no-idle-CPU rule, so revisit
when ready, possibly arcade-only): the breathing shimmer on the hovered row
and loaded ● keys, and animated pane/status transitions. Routing overlay
body text through explicit theme colors (tracked in M26) shipped in v0.9.2,
backed by a render test that fails on any unstyled overlay text.
Let users theme the UI. The styling already centralizes colors in named palette
constants (colPrimary, colAccent, …) — lift them into a Theme struct built
from those defaults, and let config.toml override any entry (hex or 256-color
index) under a [theme] table. Honors M24’s NO_COLOR (theme ignored when
color is off; unknown/partial themes fall back per-field to defaults).
config.toml.textStyle now; overlay bodies
(perms/known_hosts/copy/edit/help) still print some plain text that inherits the
terminal’s foreground (e.g. a user with green terminal text saw green labels) —
wrap them so the theme fully controls color.Status — core done. Shipped: Theme struct (fg + full-screen Bg), 16
built-in presets (default, mono, high-contrast, dracula, nord, gruvbox-dark/light,
solarized-dark/light, catppuccin-mocha/macchiato/frappe/latte,
tokyonight/-storm/-day), in-app picker (t) with live preview + windowing for
short terminals, randomize (r) / reset, and persistence to config.toml
(theme = "..."). Full-screen background fill via applyBackground — re-asserts
the bg escape after every inner reset and fills to terminal width+height, so the
whole screen takes the theme color (not just where text sits).
Deferred: per-field hex overrides under a [theme] table (presets only for now).
Routing overlay body text through textStyle (the terminal-fg bleed above —
shared with M25) shipped in v0.9.2, along with a WCAG contrast gate over every
preset palette.
Real configs use Match blocks and directives sshush doesn’t model. Today
Match blocks round-trip on write (untouched) but aren’t surfaced. Surface them
read-only first (display, flag non-editable), then allow edits where the AST
round-trips. Audit common directives (ProxyJump, IdentitiesOnly,
AddKeysToAgent) — they flow through Options, but verify display.
Status — read-only done. Match blocks now surface in the Hosts pane keyed
and labeled by their criteria (Match Host … / Match all) with a read-only
tag, parsed via the library’s unexported isMatch/matchKeyword (pinned to
ssh_config v1.6.0, reflection-guarded like isImplicitHost). Their directives
(incl. ProxyJump etc., which flow through Options) are parsed for display.
Every mutator — SetHostField, DeleteHostField, AddHostIdentity,
RemoveHostIdentity, DeleteHost — refuses a Match block with
ErrMatchReadOnly, and the TUI blocks e/d/i/enter on one. Directive
display audited: ProxyJump/IdentitiesOnly/AddKeysToAgent round-trip and
show in the edit overlay.
Deferred: editing Match blocks (the “edit where the AST round-trips” half);
surfacing key directives (e.g. ProxyJump) in the host row, not just the editor.
Note: the parser only accepts Match all / Match Host …; other criteria
(Match user, Match exec, …) fail to parse upstream — a library limit, not
handled here.
Every write backs up to <path>.bak before the first change of a session (M7).
Expose an undo: restore <path> from <path>.bak (confirm-gated), so a bad edit
is one keystroke to revert. Surface in the TUI and as a sshush restore
subcommand.
Status — done. FileRepo.BackupPaths()/Restore() revert every loaded file
(main + Includes) that has a sibling .bak to that pre-edit snapshot; the
service exposes CanRestore/BackupPaths/RestoreBackup (restore + refresh).
In the TUI, R opens a confirm gate listing the files to revert (or reports
“no backup” when none); sshush restore does the same non-interactively. The
.bak is the snapshot from before sshush’s first edit of the session, so
restore reverts the whole session’s writes — coarser than per-edit undo, matching
the backup model. Granularity note: a stack of timestamped backups for true
step-by-step undo is possible later but unscoped.
Unit tests are thorough; add e2e coverage behind a build tag: spin a real
ssh-agent, generate a throwaway key, exercise load/unload/delete and a config
edit against a temp ~/.ssh. CI matrix (ubuntu + macOS) runs the full suite incl.
e2e, plus the existing gofmt/go vet gates.
Status — done. pkg/service/e2e_test.go (//go:build e2e) starts a private
ssh-agent, generates a passphrase-less ed25519 key, and walks the real wiring
(disk scanner + config repo + agent client): generate → load → assert
LoadedInAgent (fingerprint match) → unload → edit a config host (asserting the
write + .bak) → delete (asserting files gone). Run locally with
go test -tags e2e ./pkg/service/. CI is now a {ubuntu, macOS} matrix running
gofmt/vet/go test ./... then go test -tags e2e ./....
Distribution: a Homebrew tap (goreleaser publishes the formula), an AUR
PKGBUILD, shell completions (bash/zsh/fish from the subcommand set), and a man
page. Mostly goreleaser config + a tap repo.
Status — config + assets done; publishing needs infra. Shipped: static
completions under cmd/sshush/completions/ (embedded, printed by
sshush completion <bash|zsh|fish>, packaged in release archives), a roff man
page man/sshush.1, and goreleaser brews:/aurs: blocks plus archive files:
that bundle completions + man + LICENSE. The Homebrew formula installs the
binary, all three completions, and the man page; the AUR sshush-bin package
does the same under standard prefixes.
Remaining (user infra, can’t be done from the repo): create the tap repo
s-johri/homebrew-tap and add a HOMEBREW_TAP_TOKEN secret (the default
GITHUB_TOKEN can’t push to a second repo); register the AUR sshush-bin
package and add the AUR_KEY secret. Both are wired into release.yml and
no-op until the secrets exist. Note: goreleaser deprecates brews in favor of
homebrew_casks; the formula path still works under ~> v2 and is conventional
for a CLI — migrate if a future v2 drops it.
Pre-1.0 hardening: audit every error path for graceful degradation (no panics on
malformed config, missing agent, unreadable keys, permission errors); freeze the
config.toml schema (document it, forward-compatible parsing); add
screenshots/asciinema to the README; start a CHANGELOG.md. The RC (v0.9.0)
ships after this; v1.0.0 follows a soak period of bug-fix-only patches.
Status — done (four passes): (1) CHANGELOG.md backfilled v0.1.0→v0.7.0
(Keep a Changelog + compare links); (2) config.toml schema frozen and
documented, with Warnings() surfacing unknown keys to stderr
(forward-compatible — unknown keys ignored, never fatal); (3) error-path audit —
scanner degrades per-key, indexing/assertions all guarded, no panic/os.Exit
in pkg/internal, and the one silent swallow (a malformed config.toml in
runTUI) now warns and falls back to defaults; (4) docs polish — a vhs demo
tape (docs/demo.tape) plus README demo/changelog sections. Ready to tag the
v0.9.0 RC. (The demo GIF itself is generated by running vhs docs/demo.tape.)
See the “Beyond v1.0” table above. Brief notes:
ssh-copy-id -i <pub> <user@host> via
tea.ExecProcess.ssh-add -t <ttl> and -c; show
remaining lifetime in the Keys pane (needs agent key-lifetime introspection).ssh-keygen -p (interactive)..pub without private; keys no host references — reuse hostsByKey).ssh-keygen -lv -f <key> in a detail overlay.ssh -o BatchMode=yes -o ConnectTimeout=… -T <host> →
up/down/auth badge; run async, never block the UI.Tests ride alongside each pkg milestone, not deferred. Parse/write corruption = worst-case bug; round-trip test guards it.