A fast, modern SSH + container manager for your terminal.
SSHM is a TUI & CLI tool written in Rust to manage and connect to SSH hosts, Docker containers, Incus instances, and Kubernetes pods — all from one keyboard-driven interface.
Built for developers, sysadmins, pentesters, and homelab folks who live in a terminal.
- Host management — add, edit, delete, tag, organize into nested folders
- Clone host —
yduplicates the selected host (tunnels included) and drops you straight into the editor - Fuzzy search + prefix filters —
tag:prod host:10.* user:ubuntu, fzf-style scoring - Tunnels — saved per-host port forwards (local
-L, remote-R, dynamic SOCKS-D); start them in the background and watch / stop them from thetdashboard, or list and stop them from anywhere withsshm tunnel - Tunnels that come up on connect — opt-in per tunnel; connecting to the host from the TUI starts its marked tunnels in the background first
- Auto-restart a dropped tunnel — opt-in per tunnel; relaunched with a growing backoff and given up on after 5 tries, so a Wi-Fi blip doesn't cost you the forward
- Multi-hop ProxyJump —
bastion1,bastion2, each entry resolves against your saved hosts automatically - Identity management — push SSH public keys, generate new keys (
ed25519,ed25519-skFIDO2,ecdsa,rsa), load intossh-agent - ForwardAgent (
-A) per host — opt-in with a visible warning, badged in the list - Mosh per host — opt-in toggle; connects via
moshinstead ofssh, forwarding port / identity / ProxyJump automatically - Run-on-connect — a per-host command run at login (
RemoteCommand+-t), then you land in a normal shell; start it withexecto take over the session yourself - Raw ssh options per host — a
;-separated list of-osettings (ServerAliveInterval=30,SetEnv=FOO=bar,Ciphers=…) for everything the dedicated fields don't cover. They apply to the interactive connection, background tunnels and fan-out alike, and are written out byexport - Copy connection string —
Ycopiesuser@hostto the clipboard (pbcopy/wl-copy/xclip/xsel) - Per-host notes — free-text reminder shown in the detail panel
- Hardware key detection —
[HW]badge for*-skkeys - Frecency sort + Recently Used —
scyclesname → MRU → most-used → favorites → frecency - Group by tag —
gtoggles between folder view and tag view - Bulk actions —
Spaceselects,Tadds tags to selection,Ddeletes,Cclears - Fan-out —
Xruns one command on every selected host over SSH, with per-host output and an ok/failed summary - Quick connect —
1-9connects to the Nth visible host - Health probes — periodic TCP + SSH banner check, latency in ms, banner version (
OpenSSH_9.6) shown inline
A dedicated tab between Hosts and Identities to manage containers and pods:
- Docker (local) — auto-detected if
dockeris on PATH and the daemon is up - Docker (remote) — pick any saved SSH host, sshm sets
DOCKER_HOST=ssh://...and tunnels everything natively. No port to open, no TLS, no socket setup - Apple
container(macOS) — Apple's native container runtime (macOS 26+, Apple silicon) auto-detected when thecontainerCLI and its system service are up. Lists / shells / logs / start-stop, same as Docker - Podman (local) — auto-detected when
podman infoanswers, rootless included. Same list / shell / logs / inspect / start-stop as Docker, because podman speaks the same CLI. Remotes are not wired yet - Incus (local) — auto-detected, lists containers and VMs
- Incus (remote) — auto-imported from
incus remote list - Kubernetes / K3s — auto-imported from every context in
~/.kube/configand$KUBECONFIG - One Enter to shell into any container / pod / instance —
/bin/shdirectly, no bash dance (override withkluster_shellfor images that ship it elsewhere) - Rich detail view —
iopens a scrollable inspect panel: overview (image, status, CPU/mem, platform), networking (IPs, gateway, MAC), published ports, volumes, entrypoint/command, and a live log tail. Backed bydocker/container inspect - One
lto follow logs —Ctrl+Creturns to the TUI cleanly (no app exit) - Lifecycle control —
sstarts/stops andRrestarts Docker/Apple containers and Incus instances right from the list - Pod cleanup —
don aSucceeded/Failedpod runskubectl delete pod - Section folding — clusters collapsed by default,
Enteron a header toggles - Live filter —
/fuzzy-filters containers, pods and instances across every section (force-expands while filtering) - Live discovery — background worker polls every
kluster_refresh_secs(configurable in Settings)
- i18n — tab bar, forms, dialogs, shortcut bar and messages are all translated; English + French bundled. Pick via
SSHM_LANG=fr. A few screens (Settings, Theme, the Kluster tab's own labels) are still English-only - Themes — nine customizable color roles via
theme.toml(bg,fg,accent,muted,error,success, pluswarning,borderandselection), with an optional transparent background that uses the terminal's own - Toast notifications — non-intrusive feedback for actions
- Desktop notifications — native OS alerts (
notify-send/osascript) when a background tunnel drops or a host changes reachability - Open in a new terminal —
olaunches the SSH session in a separate terminal window (auto-detected, or setexternal_terminal) - Host-key trust — vet fingerprints with
F; on connect, a never-seen host offers trust-on-first-use with its fingerprint shown, and a changed host key is detected and offers to wipe the staleknown_hostsentry and reconnect - Auto-export — optionally writes a clean
~/.ssh/configon every save - Config sync over git — keep your hosts, clusters and theme in a private git repo, authenticated with an SSH key. Manual, scheduled or cron-driven; several running instances share one schedule and only one of them ever syncs. Optionally encrypted with
age, so the remote never holds your inventory in clear - CLI mode — scriptable commands for automation
brew tap Sn0wAlice/sshm https://github.com/Sn0wAlice/sshm
brew install sshmGrab the latest binary from the Releases page.
Linux (amd64)
curl -sL https://github.com/Sn0wAlice/sshm/releases/latest/download/sshm-linux-amd64.tar.gz | tar xz
sudo mv sshm /usr/local/bin/Linux (arm64)
curl -sL https://github.com/Sn0wAlice/sshm/releases/latest/download/sshm-linux-arm64.tar.gz | tar xz
sudo mv sshm /usr/local/bin/macOS (Apple Silicon)
curl -sL https://github.com/Sn0wAlice/sshm/releases/latest/download/sshm-darwin-arm64.tar.gz | tar xz
sudo mv sshm /usr/local/bin/git clone https://github.com/Sn0wAlice/sshm.git
cd sshm
cargo build --release
sudo cp target/release/sshm /usr/local/bin/Requirements:
- Rust stable toolchain (build only)
sshclient (always)dockerCLI on PATH (for the Docker section of the Kluster tab — local and remote)kubectlon PATH (for k8s/k3s clusters)incusCLI on PATH (for Incus instances)- A terminal with UTF-8 & ANSI support
The Kluster tab degrades gracefully — sections show (unavailable) when the corresponding CLI / daemon isn't reachable.
The repo is a Cargo workspace:
crates/sshm-core # frontend-agnostic engine (models, config IO, ssh/kluster, filter, i18n)
crates/sshm # the TUI + CLI (binary `sshm`)
sshmThe TUI has 6 tabs (← / → to switch):
| Tab | Purpose |
|---|---|
| Hosts | SSH host list with folders, tags, tunnels, identity management |
| Kluster | Docker / Incus / k8s containers and pods |
| Identities | Local SSH keys (~/.ssh), generate, push, load into agent |
| Settings | Defaults, health-check intervals, kluster refresh, etc. |
| Theme | Pick / customize TUI colors |
| Help | In-app help |
- Add an SSH host in the Hosts tab pointing at a machine where Docker runs.
- Make sure your SSH user is in the
dockergroup on that host (ssh user@host docker psshould work). - In the Kluster tab, navigate to the
Docker (local)header and pressn. - Pick the host from the list. Done — sshm tunnels every
dockercall over SSH.
No ports opened, no TLS to set up, no dockerd socket exposed.
sshm list [--filter "expr"] # list hosts (filter: tag:foo host:1.* user:bar name:*xyz*)
sshm list --json # the same hosts as a JSON array
sshm list --names # one host name per line (what completions consume)
sshm connect <name> [ssh-options...] # connect to a host (alias: c)
sshm create # interactively create a host
sshm edit # edit an existing host
sshm delete # delete a host
sshm tag add <name> <tag1,tag2> # add tags
sshm tag del <name> <tag1,tag2> # remove tags
sshm load_local_conf # import hosts from ~/.ssh/config
sshm export [path] # export DB as ~/.ssh/config format
sshm add-identity <name?> [--pub key] # push pubkey to authorized_keys
sshm tunnel [list] [--json] # background tunnels across every running instance
sshm tunnel stop <pid> # terminate one tunnel by its ssh PID
sshm sync # sync the config with your git repo
sshm sync setup|status|pull|push|cron # configure / inspect / one-way / crontab line
sshm sync status --json # the same status as a JSON object
sshm doctor [--json] # what's set up, what's missing, what's wrong
sshm completions bash|zsh|fish # print a shell completion script
sshm help # full CLI reference
sshm version # print the version (--version, -V)sshm doctorReports the resolved config directory and whether each file parses, which CLIs
are on PATH (and whether Docker's and Podman's daemons actually answer), ~/.ssh
and key permissions, whether a sync run would succeed and whether its encryption
is set up, and any tunnel records left behind by a crashed instance.
An absent optional CLI is reported as skipped, not failed — a machine without
incus is not a broken machine. The exit status is 1 only when something is
genuinely broken, so it can gate a script:
sshm doctor >/dev/null || echo "sshm needs attention"
sshm doctor --json | jq -r '.[] | select(.status=="fail") | "\(.name): \(.detail)"'Three commands take --json — list, sync status and tunnel list — and
all of them stay well-formed when there is nothing to report (an empty array,
not a sentence), so a caller never has to special-case "no results":
sshm list --json | jq -r '.[] | select(.tags[]? == "prod") | .name'
sshm sync status --json | jq -e '.problem == null' # exit 1 when sync is broken
sshm tunnel list --json | jq '.[] | {pid, route}'sshm list --names prints one host name per line — no quoting to undo, no
JSON parser needed — which is what the completion scripts consume.
Install completions for your shell:
sshm completions bash > /usr/local/etc/bash_completion.d/sshm
sshm completions zsh > "${fpath[1]}/_sshm"
sshm completions fish > ~/.config/fish/completions/sshm.fishThey complete subcommands, and — for connect, add-identity and tag —
your actual saved hosts. Host names come from sshm list --names at
completion time, so adding a host is enough; there is nothing to regenerate.
SSHM_VERBOSE=1 puts the "Loading DB from …" diagnostic back, on stderr.
| Key | Action |
|---|---|
← / → |
Switch tabs |
q |
Quit |
| Key | Action |
|---|---|
↑ / ↓ |
Navigate |
Enter |
Connect to host / expand-collapse folder |
/ or any letter |
Activate fuzzy filter |
1-9 |
Quick-connect to Nth visible host |
s |
Cycle sort mode (name / MRU / most used / favorites / frecency) |
g |
Toggle group-by-folder ⇆ group-by-tag |
f |
Toggle favorite on selected host |
c |
One-shot health check on selected host |
| Key | Action |
|---|---|
a |
Add a host (or folder when on a folder row) |
e |
Edit selected host |
y |
Clone selected host (full copy, opens the editor) |
Y (Shift+y) |
Copy the connection string (user@host) to the clipboard |
d |
Delete selected host / folder |
p |
Open port-forward menu — start a tunnel in the background (f runs it foreground). Space on Start automatically brings it up whenever you connect to the host; on Restart automatically it comes back if it drops |
t |
Background-tunnels dashboard — d/x stop a tunnel, o open a local tunnel's URL |
o |
Open the SSH session in a new terminal window |
F (Shift+f) |
Host key — show the pinned vs. live fingerprint, then pin (trust), forget, or replace it |
i |
Push identity to selected host |
r |
Rename folder |
Space |
Toggle host in bulk selection |
T (Shift+t) |
Bulk-add tags to selected hosts |
D (Shift+d) |
Bulk-delete selected hosts (with confirm) |
C (Shift+c) |
Clear bulk selection |
X (Shift+x) |
Fan-out: run a command on every selected host |
The available actions depend on what's under the cursor.
| Key | When | Action |
|---|---|---|
↑/↓ j/k |
always | Navigate |
/ |
always | Fuzzy-filter containers / pods / instances (Esc clears) |
Enter |
on a header | Expand / collapse the section |
Enter |
on a container / pod / instance | Open /bin/sh (Ctrl+D to exit) |
i |
on a container / pod / instance | Open the rich detail (inspect) panel — scroll with ↑/↓, Esc closes |
l |
on a container / pod / instance | Stream logs -f (Ctrl+C returns to TUI) |
s |
on a Docker/Apple container / Incus instance | Start it if stopped, stop it if running |
R (Shift+r) |
on a Docker/Apple container / Incus instance | Restart it |
r |
always | Force a refresh now |
n |
on a Docker header | Pick a saved host → register a Docker remote |
n |
elsewhere | Add a new k8s/k3s cluster (TUI form) |
e |
on a Cluster header | Edit cluster (kubeconfig / context / namespace) |
d |
on a Cluster header | Unlink cluster from sshm (cluster itself untouched) |
d |
on a Docker remote header | Unlink Docker remote (host still in Hosts tab) |
d |
on a Succeeded / Failed pod | kubectl delete pod (with confirm) |
| Key | Action |
|---|---|
↑ / ↓ |
Navigate keys in ~/.ssh |
/ |
Fuzzy-filter keys by file name / type / comment (Esc clears) |
g |
Generate a new key (interactive: ed25519 / ed25519-sk / ecdsa / rsa) |
p |
Push selected pubkey to a host |
a |
Add selected key to ssh-agent |
x |
Remove selected key from ssh-agent |
K (Shift+k) |
Clean a hostname from ~/.ssh/known_hosts |
r |
Rescan ~/.ssh |
Everything lives in one directory, written <config> below. It is not the
same place on every OS — sshm uses the platform's standard config location:
| OS | <config> |
|---|---|
| Linux / BSD | ~/.config/sshm/ |
| macOS | ~/Library/Application Support/sshm/ |
| Windows | %APPDATA%\sshm\ |
sshm sync status prints the resolved paths it is actually using, which is the
quickest way to settle any doubt.
| Path | Purpose |
|---|---|
<config>/host.json |
Hosts, folders, tunnels, ProxyJump, per-host ssh options |
<config>/kluster.json |
Saved clusters + Incus remotes + Docker remotes |
<config>/settings.toml |
Defaults, health & kluster intervals |
<config>/theme.toml |
TUI color theme (optional) |
<config>/tunnels/<pid>.json |
Live background tunnels per running instance — used to clean up after a crash |
<config>/sync-repo/ |
Working clone used by config sync — scratch space, safe to delete |
<config>/sync-state.json |
Last sync time/result, shared by every running instance |
<config>/sync.lock |
Held while a sync runs, so only one instance syncs at a time |
<config>/sync-age.key |
Default location for the sync encryption identity (optional) |
The Settings tab (Tab → Settings) exposes:
- Default Port / Default Username / Default Identity File — used when creating new hosts
- Export Path — where to write the auto-exported
~/.ssh/config(empty = disabled) - Auto Health Check — toggle the background SSH probe
- Health Refresh / Cache TTL — seconds between probe rounds
- Probe Connect Timeout — TCP connect timeout in ms (banner read uses ~1/3)
- Kluster Refresh Interval — seconds between Docker / kubectl / Incus refreshes
- Kluster Log Tail — default
--tail Nforl(logs) - Desktop notifications — toggle native OS alerts (tunnel dropped, host up/down)
- Config sync — repository URL, SSH key, branch, auto-sync interval, and the on-start / on-exit triggers (see Config sync)
The Settings tab groups these into labelled sections (Defaults, Export, Health checks, Kluster, Notifications, Config sync).
All values are live: hit Save and the background workers pick up the new TTL on the next tick.
external_terminal — a settings.toml-only key (not shown in the Settings tab). It's the command prefix used by the o hotkey to open a session in a new terminal window; the SSH command is appended to it. Leave it empty to auto-detect (wezterm, kitty, alacritty, gnome-terminal, konsole, xterm, or Terminal.app on macOS). Examples:
external_terminal = "kitty -e"
external_terminal = "wezterm start --"
external_terminal = "gnome-terminal --"notification_icon — another settings.toml-only key: a path (~ allowed) to a custom icon for desktop notifications.
notification_icon = "~/.config/sshm/icon.png" # any path; `~` is expandedOn Linux it's passed straight to notify-send -i. On macOS the default osascript notification cannot override its icon (it's always osascript's) — install terminal-notifier (brew install terminal-notifier) and SSHM will use it automatically to honour the custom icon.
kluster_shell — a third settings.toml-only key: the shell Enter execs into a container, pod or instance. Empty means /bin/sh, which every mainstream image ships. SSHM deliberately does not probe for a nicer shell — a bash-fallback wrapper used to live here and caused more corner cases than it solved — so point this at the one your images actually have:
kluster_shell = "/busybox/sh"Keep the same hosts on your laptop, your desktop and that one server you keep forgetting about. sshm pushes its config to a git repository you own, over SSH, with your key. Nothing is sent anywhere you didn't configure.
sshm sync setup # repo URL, key, what travels, how often
sshm sync # sync now
sshm sync status # what's configured, when it last ran, who holds the lockCreate an empty private repo first (GitHub, GitLab, Gitea, a bare repo on your
own box — anything reachable over SSH), then paste its SSH URL:
git@github.com:you/sshm-config.git. HTTPS URLs are rejected: they can't
authenticate with a key.
What travels. host.json and kluster.json by default, plus theme.toml;
settings.toml is opt-in. The [sync] block itself never leaves the
machine — it holds your key path, and syncing it would point every other
machine at the same one (or switch sync off everywhere at once).
Encryption (optional, off by default). Without it the repository holds your
hosts in clear — names, addresses, usernames, ports, key paths, tags and notes.
A private repo is still a repo: mirrors, backups, org-wide access, a compromised
account. Turn it on from the Settings tab, from sshm sync setup, or by hand:
[sync]
encrypt = true
# An absolute path, or `~`-relative. Put it wherever you like — the default
# sshm suggests is inside its own config directory, which is NOT the same
# place on every OS (see "Files" above):
# Linux ~/.config/sshm/sync-age.key
# macOS ~/Library/Application Support/sshm/sync-age.key
age_identity = "~/.config/sshm/sync-age.key"It needs age on PATH (brew install age,
apt install age). sshm sync setup offers to generate the identity; copy that
one file to every machine that syncs the repo, the way you would an SSH key —
without it they cannot read what this machine pushes.
sshm always shows you the resolved path, because a ~ that expands
somewhere unexpected is the easiest way to end up with a key sshm never opens.
The Settings tab prints it under the field (green when the file is there, amber
when it is missing and encryption is on), sshm sync setup echoes it back
before generating anything, and sshm sync status reports it:
Encryption : age, identity /home/you/.config/sshm/sync-age.key
Three things worth knowing:
- Your local files stay unencrypted. This protects what leaves the machine,
not what sits on it.
host.jsonon your own disk is unchanged. - Turning it on is a non-event. Decryption is decided by looking at the blob, not at your settings, so history written before you switched it on still reads back. Turning it off again is equally undramatic.
- It never falls back to cleartext. If
ageis missing or the identity is unusable, the run aborts.sshm sync statussays so before you find out the hard way.
How conflicts resolve. Hosts and clusters are merged entry by entry
against the last state you synced, so a host added on the laptop and another
added on the desktop both survive, and a host deleted on one machine stays
deleted instead of coming back. Only the same entry edited on both sides in the
same window is a real conflict, resolved by your policy (default: this machine
wins). settings.toml and theme.toml are whole-file, so the policy decides
directly.
When it syncs — any combination of:
| Trigger | Set with |
|---|---|
| Manually | sshm sync |
| Every N minutes | Settings tab, or mode = "interval" in settings.toml |
| On start / on exit | Settings tab toggles |
| From cron | sshm sync cron prints the line, --if-due respects the interval |
Several instances at once are fine. Two TUIs and a cron entry all share one lock and one schedule through the config directory: exactly one of them syncs each round, the others skip that tick instead of piling up behind it. A crashed instance never wedges the lock — it's reclaimed once its process is gone.
# settings.toml — written by `sshm sync setup`, editable by hand
[sync]
enabled = true
repo_url = "git@github.com:you/sshm-config.git"
ssh_key = "~/.ssh/id_ed25519"
branch = "main"
mode = "interval" # or "manual"
interval_secs = 900 # floored at 60
on_start = true
on_exit = true
items = ["hosts", "kluster", "theme"] # add "settings" to sync those too
conflict = "prefer_local" # or "prefer_remote"
strict_host_key_checking = false
encrypt = false # seal the payload with `age` before committing
age_identity = "" # the identity to encrypt to / decrypt withSync shells out to your own git, so your ~/.ssh/config, agent and proxy
settings all apply. A passphrase-protected key needs to be in your ssh-agent —
sync never prompts (it would hang a background worker), it fails with a clear
error instead.
bg = "#1e1e2e"
fg = "#cdd6f4"
accent = "#89b4fa"
muted = "#6c7086"
error = "#f38ba8"
success = "#a6e3a1"
transparent_bg = false
# Optional, `theme.toml`-only (the Theme tab edits the six above).
# Each falls back to the role it used to borrow, so leaving them out
# renders exactly like an older theme.
warning = "#f9e2af" # a caution, not a failure — falls back to `error`
border = "#45475a" # box borders and separators — falls back to `muted`
selection = "#585b70" # the selected row — falls back to `accent`Set transparent_bg = true (or tick Transparent background in the Theme
tab) to drop the bg colour entirely and let your terminal's own background —
including any transparency / blur — show through. The bg hex is kept on disk
so unticking the box restores it.
SSHM_LANG=fr sshm # French
SSHM_LANG=en sshm # English (default)Falls back to the value of LC_ALL / LANG if SSHM_LANG is unset. Unknown locales fall back to English silently.
Two crates: a frontend-agnostic engine, and the terminal frontend on top of it.
Hard rule — no terminal-UI dependency (ratatui / crossterm / inquire) may appear
in sshm-core's dependency tree.
crates/sshm-core/src/ # the engine — no rendering, no event loop
├── models.rs # Host, Folder, Database
├── tunnels.rs # Tunnel model + on-disk registry
├── history.rs # frecency, sort modes
├── i18n.rs # localization
├── locales/ # en.toml, fr.toml
├── os.rs # OS integration (notifications, external terminal)
├── tty.rs # terminal handover hook (set by the frontend)
├── watch.rs # debounced config-dir change detection
├── filter/ # fuzzy + prefix-token matcher
├── config/ # io, path, settings, export
├── ssh/ # client, keys, agent, known_hosts, proxy
├── import/ # ~/.ssh/config parser
├── kluster/ # Docker / Incus / Apple container / kubectl wrappers
│ ├── engine.rs # command construction shared by docker + podman
│ ├── docker.rs # docker ps / exec / logs (local + DOCKER_HOST=ssh://)
│ ├── podman.rs # the same, against `podman` (local only)
│ ├── incus.rs # incus list / exec / logs (local + remotes)
│ ├── apple.rs # Apple `container` runtime (macOS 26+)
│ ├── kube.rs # kubectl get/exec/logs/delete pod
│ ├── shell.rs # /bin/sh constant
│ └── db.rs # kluster.json + bootstrap from kubeconfig + incus remotes
└── sync/ # git-over-SSH config sync
├── engine.rs # the run: pull, merge, push
├── crypt.rs # optional `age` encryption at the git boundary
├── git.rs # git plumbing over an SSH key
├── merge.rs # entry-by-entry three-way merge
├── lock.rs # cross-process O_EXCL lock
└── state.rs # last-run timestamp, last error
crates/sshm/src/ # the TUI + CLI (binary `sshm`)
├── main.rs # CLI dispatch
├── lib.rs # crate root
├── commands/ # CLI subcommands (list, crud, tags, connect, sync)
├── ssh/ # connect flow + add-identity wizard
└── tui/
├── app/ # main loop + worker submodules
│ ├── tab_events.rs # key handling for the self-contained tabs
│ ├── health_worker.rs
│ ├── kluster_worker.rs
│ ├── sync_worker.rs
│ ├── kluster_actions.rs
│ ├── cluster_form.rs
│ ├── host_form.rs
│ ├── tunnels.rs
│ ├── fanout.rs
│ └── key_flows.rs
├── tabs/ # one file per tab
├── ssh/ # host detail box, modals, toast, port forward
└── theme.rs
PRs welcome — especially for:
- Terminal UX polish
- New runtime backends (LXD, Podman, ...)
- Platform support (Windows is currently best-effort)
- More translations (just drop a
crates/sshm-core/src/locales/<code>.toml)
Before sending a PR, run what CI runs:
cargo fmt --all -- --check
cargo test --workspace
SSHM_LANG=fr cargo test --workspace
cargo clippy --workspace --all-targets -- -D warningsThe suite covers the engine (parsers, sync merge, cross-process lock, known_hosts, ssh argv construction) and the parts of the TUI whose state is separable from rendering (form state, the Kluster tab's selection and key handling, theme fallbacks). The French run matters: the active locale comes from the environment, so a test asserting an English literal passes on an English machine and fails on a French one.
Made by Sn0wAlice
