Run commands on many servers at once — one Python file, zero dependencies, powered by your OpenSSH client.
ssherd is a small batch-SSH CLI for server fleets. Your ~/.ssh/config (or a project-local .ssh/config) is the inventory: ssherd reads the Host aliases, fans out commands in parallel, and lets the real ssh client handle everything it already does best — addresses, jump hosts (ProxyJump), users, keys, host keys.
Works on Linux, macOS, and Windows (Git-Bash / PowerShell).
- Single file, zero dependencies — copy
ssherd.pyanywhere, needs only Python 3.9+ andssh - SSH config as inventory — selectors by name, comma list, or range (
web-01..web-16,all) - Parallel execution with a live per-device progress bar
- Smart grouped output — identical results collapse into one block, outliers shown as compact diffs (
--diff) - Jump hosts / keys / users — all resolved natively by OpenSSH, never re-implemented
- Password auth support via OpenSSH's official
SSH_ASKPASS_REQUIRE=force(no sshpass, never touches your tty) - Sudo fan-out —
sudo -Swrapping with one local password prompt add-user— generates plain, auditable bash commands by default (--execto run them)
pipx install ssherd # isolated venv + global command (recommended)
pip install ssherd # or plain pip — works on any OSRequirements: Python 3.9+ and the OpenSSH client (ssh) you already have.
Prefer no installer at all? It's one self-contained file — grab it, read it, hack it:
curl -fsSL https://raw.githubusercontent.com/trdthg/ssherd/master/ssherd.py \
-o ~/.local/bin/ssherd && chmod +x ~/.local/bin/ssherdUninstall: pipx uninstall ssherd or pip uninstall ssherd.
# the inventory is just your ssh config Host aliases
python -m ssherd list
# run in parallel across a range
python -m ssherd run web-01..web-16 -- whoami
# comma lists and single hosts work too
python -m ssherd run web-01,web-05,db-01 -- hostname
# sudo: prompts once, wraps remotely with sudo -S
python -m ssherd run --sudo db-01..db-32 -- apt update
# scriptable: pass passwords inline (note: lands in shell history)
python -m ssherd run --sudo=SUDOPASS db-01 -- systemctl restart nginx
# group identical outputs, show only the differences
python -m ssherd run all --diff -- 'cat /etc/os-release'Devices are the Host entries in your SSH config (Host * wildcards are skipped):
Host web-01
HostName 192.0.2.10
User deploy
# ProxyJump jumpbox ← jump hosts just work, ssh handles them
A .ssh/config in the current directory takes precedence over ~/.ssh/config,
so a fleet per project is one folder away. Put .ssh/ in .gitignore and real
IPs never get committed. Selectors accept aliases, comma lists, ranges
(web-01..web-16), and all / *.
add-user prints the plain bash commands (useradd / usermod / chpasswd) for you
to audit and paste, or to wrap in your own loop. Add --exec to fan them out
through the same parallel channel:
python -m ssherd add-user web-01..web-16 deploy
python -m ssherd add-user web-01..web-16 deploy --sudo --password 'TempPass-123' --execThey are great; this is for the other 90% of ops work: one-off fleet commands
where you refuse to install anything, write a playbook, or learn a DSL.
ssherd delegates all connection semantics to OpenSSH, so anything your
ssh can reach, ssherd can reach — including bastion hosts.
python -m unittest discover -v