A simple tool written in bash to make connecting to servers much easier and faster.
Repo: https://github.com/pyguy-programming/sshmgr
- Fzf-based host selection: interactive menu with fuzzy search and live preview (online status + connection details)
- Jumphost support: connect through bastion hosts via SSH
-J, configured per host - Parallel ping: check all hosts at once with fping (
sshmgr -p) - JSON configuration: easy-to-read host definitions in
~/.config/sshmgr/known_hosts.json - Custom user/port per host, with port validation (defaults to 22)
- Add hosts in a built-in form:
sshmgr -aopens one text box per field, with live validation - Remove hosts:
sshmgr -rpicks them in fzf (multi-select), or takes them as arguments - Optional kitty ssh kitten: connect through
kitten sshfor shell integration and connection reuse (toggleable)
brew tap PyGuy-Programming/sshmgr
brew install sshmgrThis pulls in the dependencies (fzf, jq, fping) automatically and installs
the manual page, so man sshmgr works.
From the cloned repository, either run the installer:
./INSTALLOr install step by step:
# 1. Install dependencies: fzf, jq, fping (and nano, or set $EDITOR)
# macOS: brew install fzf jq fping
# Debian/Ubuntu: sudo apt-get install fzf jq fping
# 2. Copy the script and install the manual page
mkdir -p "$HOME/.local/bin/sshmgr" "$HOME/.local/share/man/man1"
cp sshmgr.sh "$HOME/.local/bin/sshmgr/"
cp sshmgr.1 "$HOME/.local/share/man/man1/"
grep -q MANPATH ~/.bashrc || echo 'export MANPATH="$HOME/.local/share/man:${MANPATH:-}"' >> ~/.bashrc
# 3. Add alias and reload
echo 'alias sshmgr="bash $HOME/.local/bin/sshmgr/sshmgr.sh"' >> ~/.bashrc
source ~/.bashrc
# 4. Initialize the hosts file
sshmgr -eBoth paths end up with a working man sshmgr.
| Command | Description |
|---|---|
sshmgr |
Open the interactive fzf host selection menu |
sshmgr -e / --edit |
Open known_hosts.json in $EDITOR |
sshmgr -a / --add |
Add a host in the built-in form, or from arguments |
sshmgr -r / --remove |
Remove hosts, picked in fzf or given as arguments |
sshmgr -p / --ping |
Ping all known hosts in parallel (fping) |
sshmgr -h / --help |
Show help |
Everything is also written up in the manual: man sshmgr.
With arguments, the entry is written straight away and nothing is asked:
sshmgr -a web-server example.com # user, port, jumphost stay empty
sshmgr -a web-server example.com deploy 2222
sshmgr -a internal-db 10.0.0.50 admin 22 bastion.example.comWithout arguments, sshmgr opens a form. It is drawn by the script itself, one text box per field, and the box you are in gets the caret. Anything that would keep the entry from being written is shown on the spot:
╭─ sshmgr · new host ──────────────────────────────────────╮
│ │
│ name [ web-server ] │
│ host [ a host address is required ] │
│ user [ optional ] │
│ port [ optional ] │
│ jumphost [ optional ] │
│ │
├──────────────────────────────────────────────────────────╮
│ ! a host address is required │
╰──────────────────────────────────────────────────────────╯
⇥/⇧⇥ field ← → move ⌫ delete ^U clear ^S save esc cancel
| Key | Effect |
|---|---|
⇥ / ⏎ |
next field |
⇧⇥ / ↑ |
previous field |
← → |
move the caret inside the field |
⌫ / ⌦ |
delete a character |
^U |
clear the whole field |
^S or ^D |
write the entry, once it is complete |
esc or ^C |
quit without writing |
Blank fields are left out of the entry, so an empty port keeps the 22 default,
and names must be unique. The form needs a terminal; in a script (or with
NO_COLOR set) sshmgr falls back to asking for each field on stdin instead.
sshmgr -r opens the host list in fzf with the same preview as the connect
menu: ⇥ selects an entry and moves on, ⏎ removes everything selected, esc
cancels. Names can also be passed directly, which removes them without a prompt:
sshmgr -r old-server # remove one
sshmgr -r old-server test-box # remove severalIn the selection menu: type to filter, ↑/↓ to navigate, Enter to connect,
CTRL+Q to quit. The preview shows each host's online status (via fping, if
installed) and connection details. If the selected host has a jumphost
configured, sshmgr connects through it automatically
(ssh -p <port> -J <jumphost> <user>@<host>). Unknown options print an error
pointing to sshmgr -h.
Stored at ~/.config/sshmgr/known_hosts.json (created automatically on first
run). The script validates it with jq on startup and exits with
invalid json if it is malformed or missing the hosts key.
{
"kitten_ssh": false,
"hosts": [
{ "name": "web-server", "host": "example.com", "user": "deploy", "port": "2222" },
{ "name": "internal-db", "host": "10.0.0.50", "user": "admin", "jumphost": "bastion.example.com" }
]
}| Field | Required | Description |
|---|---|---|
name |
Yes | Display name in the menu; must be unique |
host |
Yes | Hostname or IP address |
user |
No | SSH user (defaults to current local user if omitted) |
port |
No | SSH port (defaults to 22; must be 1–65535) |
jumphost |
No | Bastion host for ssh -J; omit if unused |
Set the top-level "kitten_ssh": true to connect via kitty's ssh kitten
instead of plain ssh. It is a drop-in replacement for ssh that adds remote
shell integration, connection multiplexing (much faster reconnects) and makes
the kitty terminfo database available on the remote host.
Because the kitten only works from inside a kitty terminal, sshmgr falls back to
plain ssh with a note when kitten is missing or the script runs outside
kitty. port, user and jumphost are passed through unchanged.
This is a hang inside kitty's ssh kitten, not in sshmgr, and it only happens when the remote host asks for a password.
The kitten does not let ssh prompt in the terminal. Instead its askpass helper
asks kitty to draw the prompt as an overlay, by writing a kitty-ask escape
sequence to the tty, and then waits for kitty to write the answer back into a
shared memory segment. That wait is an endless poll with no timeout, so if kitty
never services the request, nothing is printed and nothing is read — the
connection just sits there until you interrupt it.
Use key-based authentication if you can: then no password is ever requested, the overlay is never needed, and you keep the part of the kitten that actually pays off (near-instant reconnects, remote shell integration, terminfo on the remote).
If you want to keep typing passwords, turn the overlay off and let ssh prompt
at the terminal as usual:
# ~/.config/kitty/ssh.conf
askpass sshEverything else about the kitten keeps working; the only cost is a slightly
slower first connect, because the kitten can no longer send its setup data
before ssh is done with the terminal. The same works for a single connection
with kitten ssh --kitten askpass=ssh <host>.
To confirm which path you are on, compare against plain ssh:
ssh -o BatchMode=yes <user>@<host> # fails at once, no prompt neededTips: keep the JSON valid, use descriptive names, and test jumphosts manually
first (ssh -J bastion user@target). Entries from the old plain-text format
(user@address - name in known_hosts.save) can be migrated by converting
each line into a JSON entry as above.
| Problem | Fix |
|---|---|
invalid json on startup |
Run sshmgr -e and fix the syntax (trailing commas, quotes, braces); validate with jq . ~/.config/sshmgr/known_hosts.json |
| Empty menu / host missing | Each host needs a unique name field; check the hosts array is not empty |
| Connection fails | Verify host/user/port; run sshmgr -p, then try manually: ssh -p <port> <user>@<host> (add -J <jumphost> if configured) |
| Ping shows offline but SSH works | ICMP is often blocked — ping is only an indicator, not a requirement |
command not found |
Check the alias in .bashrc (source ~/.bashrc) or reinstall via brew |
kitten ssh errors out |
Set "kitten_ssh": false, or run sshmgr from inside a kitty terminal |
| "Connecting to ..." then nothing | Password prompt hang in kitty's ssh kitten, see If it prints "Connecting to ..." |
sshmgr -a asks on stdin instead of drawing the form |
There is no terminal to draw on, e.g. piped output or cron |
Diagnostics:
man sshmgr # what am I forgetting again?
sshmgr -h # script runs?
jq . ~/.config/sshmgr/known_hosts.json # valid JSON?
which fzf fping jq ssh # dependencies present?
sshmgr -p # hosts reachable?Still stuck? Open an issue at https://github.com/pyguy-programming/sshmgr/issues
with a description, steps to reproduce, and the jq validation output.
MIT — see LICENSE.md for the full license text.