You SSH into 10 servers to check disk space. You write a quick for loop. A typo crashes one session. You're not sure which ones you've already checked. By the time you're done, 20 minutes are gone and you have a terminal full of scrollback you need to piece together.
There's a better way.
The Problem
Most developers SSH into servers one at a time. When you need to run the same command across multiple machines, your options are:
- Open 10 terminals — error-prone, slow, no audit trail
-
Write a shell loop —
for host in web-01 web-02 ...; do ssh $host "df -h"; done— works until a host is down and your loop breaks - Ansible — powerful, but you need to learn YAML DSL, write playbooks, manage inventory files. Overkill for "check disk space"
- Fabric — good Python library, but no built-in host management or CLI
None of these give you a simple CLI that just works out of the box.
Meet remote-cmd
remote-cmd is a Python CLI + API for SSH server management. It gives you:
- Host CRUD with tag-based grouping (
production,staging,web,db) - One-shot command execution on single or multiple hosts
- File upload/download via SFTP
- A Python API for scripting automation
- Zero setup —
pip installand go
v2.11.0 Release Highlights
v2.11.0 focuses on reliability, scalability, and safer operations.
- Reliability: strict known_hosts verification, wall-clock command timeouts, cancellation cleanup, and staged SFTP transfers.
- Scalability: bounded batch workers, reusable TaskRunner workers, and thread-local SQLite connections.
- Storage: SQLite schema v4 adds indexed host tags and supports transactional JSON-to-SQLite migration.
- Batch output: use the JSON format for per-host success, exit code, output, duration, and error details. Timeout and not-attempted cases are currently described in the per-host error rather than separate outcome types.
A synthetic local benchmark with 1,000 in-flight Paramiko-style commands measured about 3,003 → 2,003 threads after removing the per-command stderr-reader thread. This is a local benchmark, not a production throughput guarantee.
Compatibility Note
JSON repository get/list now return detached snapshots (matching SQLite): changing a returned object requires an explicit save call. For example:
from remote_cmd.repository.json_host_repository import JsonHostRepository
repo = JsonHostRepository("hosts.json")
host = repo.get("web-01")
host.port = 2222
repo.save(host)
See the migration notes for details.
Quick Demo
# Install
pip install remote-cmd-manager
# Add your first server
remote-cmd host add web-01 192.168.1.10 ubuntu --key ~/.ssh/id_rsa
# Run a command
remote-cmd run web-01 "uptime"
# → 14:32:10 up 45 days, 2:15, 1 user, load average: 0.08, 0.03, 0.05
# Add more servers and run across all production hosts
remote-cmd host add web-02 192.168.1.11 ubuntu --key ~/.ssh/id_rsa --tag production
remote-cmd host add db-01 192.168.1.20 ubuntu --key ~/.ssh/id_rsa --tag production --tag database
remote-cmd batch-run -t production "df -h /"
# ✓ web-01 → Disk: 32G/100G (32%)
# ✓ web-02 → Disk: 45G/100G (45%)
# ✓ db-01 → Disk: 12G/50G (24%)
Real-World Use Cases
Incident Response
# Check errors across all web servers in 3 seconds
remote-cmd batch-run -t web "journalctl -xe -n 50 | grep -i error"
Deploy Code
from getpass import getpass
from remote_cmd.core.host_manager import HostManager
manager = HostManager("hosts.json")
sudo_password = getpass("sudo password: ")
for host in manager.list_hosts(tag="staging"):
with manager.connect_to_host(host.name) as client:
client.execute("cd /app && git pull")
client.execute("pip install -r requirements.txt")
client.execute_sudo("systemctl restart app", password=sudo_password)
For unattended deployments, retrieve credentials from an external secret manager or use a narrowly scoped passwordless sudo rule. Do not hardcode sudo passwords in source.
Config Update
# Upload new nginx config and reload across all web servers
remote-cmd upload web-01 ./nginx.conf /tmp/nginx.conf
remote-cmd run web-01 "sudo cp /tmp/nginx.conf /etc/nginx/nginx.conf && sudo nginx -t && sudo systemctl reload nginx"
How It Stacks Up
| remote-cmd | ssh + shell | Ansible | Fabric | |
|---|---|---|---|---|
| Setup time | 10 seconds | None (you know ssh) | 30 min+ | 5 min |
| Host management | Built-in | Manual | Inventory YAML | Manual |
| CLI | Yes | Yes | ansible cmd | fab cmd |
| Python API | Yes | No | No (YAML) | Yes |
| Ideal for | Ad-hoc + scripts | One-off | Config mgmt | Python scripts |
Getting Started
pip install remote-cmd-manager
# Full documentation
remote-cmd --help
# GitHub
# https://github.com/Vae-Scrooge/remote-cmd
Remote CMD is stable at v2.11.0. The core API is versioned under semantic versioning, and the project is tested on Linux, macOS, and Windows with Python 3.10+. v2.11.0 removed no CLI commands; behavioral changes are documented in the migration notes.
Roadmap
- Observability and lightweight metrics
- Extensible plugin architecture
- Persistent task/result storage
- Large-scale execution and result handling
- API ergonomics and long-term compatibility
If you SSH into more than a couple servers regularly, give it a try. It'll save you the "which servers did I check again?" moment.
Top comments (2)
This is a useful middle ground between a brittle shell loop and full configuration management. The tag-based inventory is especially practical for ad-hoc checks.
Before using it in production, I'd want three behaviors to be very explicit: strict host-key verification against known_hosts, per-host timeouts/cancellation with structured exit results, and credential handling that never puts sudo passwords in code or shell history. A narrowly scoped passwordless sudo rule or an external secret provider would be safer than the password literal in the deployment example.
Clear partial-failure output would also make batch runs much easier to trust: succeeded, failed, timed out, and not attempted should be distinguishable at a glance.
Thanks, Nexus — this is a thoughtful production-readiness checklist, and I agree especially about the sudo example. Since this article was published, v2.11.0 now uses strict known_hosts verification by default, provides wall-clock command timeouts, and improves cancellation cleanup. Batch results and JSON output include per-host success, exit code, output, and error details.
Two gaps remain on my roadmap: timeout configuration is currently shared at the command/batch level rather than independently configurable per host, and timed out / not attempted are not yet separate machine-readable outcome states. Also, password="sudopass" was only a placeholder, but you’re right that it’s a poor production example. execute_sudo sends the password via stdin rather than argv or logs, but callers should retrieve it from an external secret store or use a narrowly scoped passwordless sudo rule. I’ll update the example.