# sshkeeper v0.4.0 — Model & Workflow Cleanup v0.4.0 is a structural release. The main goal is to make sshkeeper's data model match what the user sees: server profiles are real reusable objects, routes reference those objects instead of mutable text, and the TUI offers pickers for things that already exist instead of asking you to retype them. Existing databases and vaults are migrated automatically. No manual conversion is required. ## Routes are now real relationships The old implementation stored both `ProxyJump` text and a second route representation. Different commands wrote different fields, and runtime SSH used one in preference to the other. That made it possible for `edit --proxy-jump` to say "Saved" while the old structured route was still used. v0.4.0 makes `Route` canonical and stores profile hops in a normalized `server_route_hops` table with foreign keys to stable server IDs. - Renaming a bastion no longer breaks dependent routes. - Deleting a profile that is still used as a route hop is rejected and names the dependent profiles. - Self references, duplicate hops and route cycles are rejected. - `proxy_jump` and legacy `route_hops` remain compatibility projections for old databases/tools, not competing sources of truth. - Existing `group_name` strings are likewise migrated to first-class `groups` with stable IDs. ### Profile hops no longer depend on `~/.ssh/config` A route hop that references a sshkeeper profile now resolves that profile's real host, user, port and identity file from SQLite. sshkeeper generates a temporary OpenSSH config for the connection and passes it with `ssh -F`. This fixes the old accidental requirement that a sshkeeper alias such as `bastion-prod` also had to exist as a matching `Host` in the user's OpenSSH config. CLI route syntax is explicit when needed: ```bash # exact known aliases are profile references sshkeeper route set prod --jumps bastion,dmz-gw # force the interpretation sshkeeper route set prod --jumps profile:bastion,raw:ops@external-gw:2222 ``` An unprefixed value matching an existing sshkeeper alias becomes a profile hop; an unknown value remains a raw OpenSSH target. Password or key-passphrase authentication on an *intermediate* profile hop is rejected with a clear error for now. The current PTY secret flow can safely feed the target profile, but it cannot reliably route different vault secrets to multiple OpenSSH prompts in a jump chain. Key/agent bastions are supported. ## Route editor in the TUI `Ctrl+X` → **Route**, or `/` while the Route field is focused, opens an ordered route editor. - `Enter` adds an existing server profile as a hop. - `x` / Delete removes a hop. - `[` / `]` moves a hop up/down in the chain. - `/` filters the available profiles. - `raw:` remains available in the editable field for arbitrary OpenSSH targets. The server form no longer claims that a picker exists while still requiring a comma-separated string. ## Server Actions vs Manage `Ctrl+X` is now strictly about the selected server: - Connect - Connect with tunnels - Start tunnels only / in background - Port forwards - Route - Test connection - Edit - Delete Press **`m`** for global management: - Groups - Tags - Command templates - Running tunnels - Import SSH config - Export - Vault lock / password change Groups now have their own manager with server counts, create/rename/delete operations, and safe delete consequences. ## Context-aware server form The form now exposes only authentication fields that matter: | Auth | Fields | |------|--------| | password | Password | | key | Identity File | | key_passphrase | Identity File + Key passphrase | | agent | no credential fields | `/` opens context-specific pickers: - Auth method → auth list - Identity File → detected private keys in `~/.ssh` - Route → profile/chain editor - Group → existing groups - Startup Command → global command templates (the command is copied, not linked) - Tags → multi-select existing tags; new tags can still be typed manually ## CLI consistency fixes All writers now go through the same route semantics. - `add`, `edit`, `route set`, TUI save and SSH config import all create the same canonical route model. - SSH config import is two-pass: profiles are created first, then `ProxyJump` aliases are resolved against the complete imported/existing profile set. - `edit` uses Cobra's `Changed()` state, so `--group ''`, `--notes ''`, `--startup-command ''`, `--identity-file ''`, `--proxy-jump ''` and `--tags ''` can actually clear values. - An empty server `User` no longer produces the invalid target `@host`; OpenSSH is allowed to choose its configured/current user. - Shared server/auth/route validation replaced duplicated partial checks. ## Vault identity follows the server, not its alias New and migrated server secrets are stored under stable server IDs. Renaming a profile no longer requires a risky copy/delete of secrets keyed by alias. Legacy `server::` records remain readable and are lazily migrated when used. `vault list` understands both legacy and stable-ID records and resolves stable IDs back to current aliases when the database is available. TUI server save now commits the database change before rewriting vault state; if the vault step fails, the profile/tags are rolled back instead of leaving the database and vault disagreeing. ## Port forward fixes Two concrete TUI bugs are fixed: - Editing a disabled forward no longer silently re-enables it. - Remote-forward preview now uses the same semantic builder as validation/save, so listen/target endpoints cannot be shown reversed. The editor has an explicit Enabled toggle and the forward list supports `Space` to enable/disable a rule quickly. ## Tunnel fixes Profile routes work consistently for foreground and background tunnels. Background tunnels keep their generated temporary SSH config for the lifetime of the process and remove it on stop/stop-all. Tunnel Manager now reports `running` separately from `tracked`; stale tracked state is no longer counted as a running process. ## Cleanup - Removed the stale per-server `CommandTemplate.ServerID` field; command templates are global. - Removed an unused legacy `model.Secret` type; the encrypted vault has its own actual storage model. - Added regression coverage for stable route references, deletion protection, cycle detection, stable-ID vault metadata, contextual auth fields, Manage, route/identity/tag/startup pickers, and forward semantics. ## Upgrade notes On first start, v0.4.0 automatically creates `groups` and `server_route_hops`, links existing group names, and converts existing route text/JSON. The old columns are retained for compatibility. As always, keep a copy of `~/.local/share/sshkeeper/` before a major upgrade if the data matters to you. ## Install Debian/Ubuntu (amd64): ```bash sudo apt install ./sshkeeper_0.4.0-1_amd64.deb ``` Fedora/RHEL-family (x86_64): ```bash sudo dnf install ./sshkeeper-0.4.0-1.x86_64.rpm ``` ARM64 packages (`arm64.deb` / `aarch64.rpm`) and the original tar.gz archives are published alongside them. Package dependencies pull in the distro OpenSSH client; user config, database and vault files are not owned or modified by the package. Verify downloads against `checksums.txt`. Linux and macOS are the primary release targets. Windows remains experimental and requires OpenSSH Client (`ssh.exe`) in `PATH`.