Skip to content

Latest commit

 

History

136 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SSHM

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.

What's in the box

Hosts (SSH)

  • Host management — add, edit, delete, tag, organize into nested folders
  • Clone hosty duplicates the selected host (tunnels included) and drops you straight into the editor
  • Fuzzy search + prefix filterstag: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 the t dashboard
  • Multi-hop ProxyJumpbastion1,bastion2, each entry resolves against your saved hosts automatically
  • Identity management — push SSH public keys, generate new keys (ed25519, ed25519-sk FIDO2, ecdsa, rsa), load into ssh-agent
  • ForwardAgent (-A) per host — opt-in with a visible warning, badged in the list
  • Mosh per host — opt-in toggle; connects via mosh instead of ssh, 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 with exec to take over the session yourself
  • Copy connection stringY copies user@host to the clipboard (pbcopy / wl-copy / xclip / xsel)
  • Per-host notes — free-text reminder shown in the detail panel
  • Hardware key detection[HW] badge for *-sk keys
  • Frecency sort + Recently Useds cycles name → MRU → most-used → favorites → frecency
  • Group by tagg toggles between folder view and tag view
  • Bulk actionsSpace selects, T adds tags to selection, D deletes, C clears
  • Fan-outX runs one command on every selected host over SSH, with per-host output and an ok/failed summary
  • Quick connect1-9 connects to the Nth visible host
  • Health probes — periodic TCP + SSH banner check, latency in ms, banner version (OpenSSH_9.6) shown inline

Kluster — Docker, Incus, k8s/k3s

A dedicated tab between Hosts and Identities to manage containers and pods:

  • Docker (local) — auto-detected if docker is 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 the container CLI and its system service are up. Lists / shells / logs / start-stop, same as Docker
  • 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/config and $KUBECONFIG
  • One Enter to shell into any container / pod / instance — /bin/sh directly, no bash dance
  • Rich detail viewi opens 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 by docker/container inspect
  • One l to follow logsCtrl+C returns to the TUI cleanly (no app exit)
  • Lifecycle controls starts/stops and R restarts Docker/Apple containers and Incus instances right from the list
  • Pod cleanupd on a Succeeded/Failed pod runs kubectl delete pod
  • Section folding — clusters collapsed by default, Enter on 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)

Quality of life

  • i18n — UI strings translatable; English + French bundled. Pick via SSHM_LANG=fr
  • Themes — fully customizable colors via theme.toml, 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 terminalo launches the SSH session in a separate terminal window (auto-detected, or set external_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 stale known_hosts entry and reconnect
  • Auto-export — optionally writes a clean ~/.ssh/config on 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
  • CLI mode — scriptable commands for automation

Installation

Homebrew (macOS / Linux)

brew tap Sn0wAlice/sshm https://github.com/Sn0wAlice/sshm
brew install sshm

Download pre-built binary

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

Build from source

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)
  • ssh client (always)
  • docker CLI on PATH (for the Docker section of the Kluster tab — local and remote)
  • kubectl on PATH (for k8s/k3s clusters)
  • incus CLI 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.

Desktop GUI (Tauri 2)

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`) — unchanged
crates/sshm-gui    # the desktop app (binary `sshm-desktop`), Svelte + Tauri 2

The GUI shares the exact same on-disk database (~/.config/sshm/*), so a host added in the terminal shows up in the app live, and vice-versa. It's a launcher: ssh sessions open in your external terminal (never embedded), and it never reads or writes private-key contents — only paths, exactly like the TUI.

# One-time: install the frontend toolchain
cd crates/sshm-gui
npm install

# Dev (hot-reload UI + Rust)
npm run tauri dev        # alias: gui:dev

# Production bundle (.app / .dmg / .deb / AppImage)
npm run tauri build      # alias: gui:build

Requirements: Node 18+ and the Tauri prerequisites for your OS (macOS: Xcode CLT; Linux: libwebkit2gtk-4.1-dev, libgtk-3-dev, librsvg2-dev). Typed IPC (tauri-specta) derives the TypeScript types from the Rust structs, so the frontend and backend never drift — the generated crates/sshm-gui/src/lib/bindings.ts is refreshed on every tauri dev, or headlessly with cargo test -p sshm-desktop export_bindings.

cargo build --release at the workspace root builds only the TUI (target/release/sshm); the desktop app is built explicitly (-p sshm-desktop or via the Tauri CLI).

Usage

TUI (recommended)

sshm

The 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

Kluster Docker remote — quickstart

  1. Add an SSH host in the Hosts tab pointing at a machine where Docker runs.
  2. Make sure your SSH user is in the docker group on that host (ssh user@host docker ps should work).
  3. In the Kluster tab, navigate to the Docker (local) header and press n.
  4. Pick the host from the list. Done — sshm tunnels every docker call over SSH.

No ports opened, no TLS to set up, no dockerd socket exposed.

CLI commands

sshm list [--filter "expr"]              # list hosts (filter: tag:foo host:1.* user:bar name:*xyz*)
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 sync                                # sync the config with your git repo
sshm sync setup|status|pull|push|cron    # configure / inspect / one-way / crontab line
sshm help                                # full CLI reference

Keyboard shortcuts

Global

Key Action
/ Switch tabs
q Quit

Hosts tab — list navigation

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

Hosts tab — actions

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)
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

Kluster tab

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)

Identities tab

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

Configuration

Files

Path Purpose
~/.config/sshm/host.json Hosts, folders, tunnels, ProxyJump
~/.config/sshm/kluster.json Saved clusters + Incus remotes + Docker remotes
~/.config/sshm/settings.toml Defaults, health & kluster intervals
~/.config/sshm/theme.toml TUI color theme (optional)
~/.config/sshm/tunnels/<pid>.json Live background tunnels per running instance — used to clean up after a crash
~/.config/sshm/sync-repo/ Working clone used by config sync — scratch space, safe to delete
~/.config/sshm/sync-state.json Last sync time/result, shared by every running instance
~/.config/sshm/sync.lock Held while a sync runs, so only one instance syncs at a time

Settings

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 N for l (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"

On 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.

Config sync (git over SSH)

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 lock

Create 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).

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, the desktop app 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

Sync 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.

Theme example

bg = "#1e1e2e"
fg = "#cdd6f4"
accent = "#89b4fa"
muted = "#6c7086"
error = "#f38ba8"
success = "#a6e3a1"
transparent_bg = false

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.

Localization

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.

Architecture

src/
├── main.rs               # CLI dispatch
├── lib.rs                # crate root
├── models.rs             # Host, Tunnel, Database
├── history.rs            # frecency, sort modes
├── i18n.rs               # localization
├── locales/              # en.toml, fr.toml
├── filter/               # fuzzy + prefix-token matcher
├── config/               # io, path, settings, export
├── ssh/                  # client, keys, agent, known_hosts, proxy
├── import/               # ~/.ssh/config parser
├── kluster/              # Docker / Incus / kubectl wrappers
│   ├── docker.rs         #   docker ps / exec / logs (local + DOCKER_HOST=ssh://)
│   ├── incus.rs          #   incus list / exec / logs (local + remotes)
│   ├── kube.rs           #   kubectl get/exec/logs/delete pod
│   ├── shell.rs          #   /bin/sh constant
│   └── db.rs             #   kluster.json + bootstrap from kubeconfig + incus remotes
├── tui/
│   ├── app/              # main loop + worker submodules
│   │   ├── health_worker.rs
│   │   ├── kluster_worker.rs
│   │   ├── kluster_actions.rs
│   │   ├── cluster_form.rs
│   │   ├── host_form.rs
│   │   └── key_flows.rs
│   ├── tabs/             # one file per tab
│   ├── ssh/              # host detail box, modals, toast, port forward
│   └── theme.rs
└── commands/             # CLI subcommands

Contributing

PRs welcome — especially for:

  • Terminal UX polish
  • New runtime backends (LXD, Podman, ...)
  • Platform support (Windows is currently best-effort)
  • More translations (just drop a src/locales/<code>.toml)

Run cargo test before sending a PR — the suite covers parsers (filter, kubeconfig, ssh_config, JSON migrations) and a handful of pure logic units (frecency, ssh banner, ProxyJump resolver, etc.).


Made by Sn0wAlice

About

sshm is a command-line tool written in Rust that makes it easy to manage a list of SSH hosts. It allows you to list, create, edit, delete, and connect to SSH hosts through an interactive terminal interface.

Topics

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages