One binary. Zero open ports. JSON output. Agent-friendly.
Execute commands on 1 or 100 machines with a single command.
Built for AI agents — structured JSON, deterministic timeouts, zero parsing.
WebSocket relay + token auth. NAT-proof. Agent-friendly.
☁️ Hosted relay — €5/mo, unlimited targets
# On a VPS — start a relay (one time)
remotecmd-cli relay daemon start --port 3032 -daemon
# On any target machine — connect to the relay
remotecmd-cli pair listen --name myserver
# Paste the one-liner on the target. Done.
# From anywhere — execute commands
remotecmd-cli --target myserver --cmd 'uptime'
# JSON output, zero parsing
# {"ok":true,"stdout":"myserver\n","exit_code":0,"duration_ms":8}Install: curl -sSL https://github.com/javimosch/remotecmd-cli/releases/latest/download/remotecmd-cli-linux-amd64 -o /usr/local/bin/remotecmd-cli && chmod +x /usr/local/bin/remotecmd-cli
SSH needs open ports, key management, VPN for NAT, and its output is text — AI agents can't parse it reliably. remotecmd uses a WebSocket relay: daemons connect out (no inbound ports), the relay routes commands by name + token, and every command returns structured JSON.
Client ──ws──► Relay Hub ◄──ws── Target Daemon
- Run a relay on any VPS (one
daemon startcommand) - Daemons on your servers connect out to the relay
- Clients send commands to the relay → relay routes to the target
- Result comes back as JSON — always structured, never text to parse
- Zero inbound ports — daemons connect out. No firewall rules.
- Token auth — auto-generated per target. No SSH keys.
- Agent-first — every command returns
{"stdout":"...","exit_code":0} - NAT-proof — works from anywhere (Tailscale, corporate proxy, 4G)
# Linux/amd64
curl -sSL https://github.com/javimosch/remotecmd-cli/releases/latest/download/remotecmd-cli-linux-amd64 \
-o ~/.local/bin/remotecmd-cli && chmod +x ~/.local/bin/remotecmd-cli
# Linux/arm64
curl -sSL https://github.com/javimosch/remotecmd-cli/releases/latest/download/remotecmd-cli-linux-arm64 \
-o ~/.local/bin/remotecmd-cli && chmod +x ~/.local/bin/remotecmd-cli
# macOS (Apple Silicon)
curl -sSL https://github.com/javimosch/remotecmd-cli/releases/latest/download/remotecmd-cli-darwin-arm64 \
-o /usr/local/bin/remotecmd-cli && chmod +x /usr/local/bin/remotecmd-cliOr build from source:
git clone https://github.com/javimosch/remotecmd-cli.git
cd remotecmd-cli && go build -o remotecmd-cli .remotecmd-cli alias install
# Installs: rc (full CLI), rcx (execute), rcl (list), rcs (status), rcc (copy)On any VPS or machine reachable by both sides:
remotecmd-cli relay daemon start --port 3032 -daemonOption A: Pair (easiest — send one line to any machine)
remotecmd-cli set-relay --url http://<relay-host>:3032 --name myclient
remotecmd-cli pair listen --name myserverIt prints a one-liner. Send it to the remote machine — they paste it:
curl -sSL https://raw.githubusercontent.com/javimosch/remotecmd-cli/master/install.sh \
| sh -s -- --relay http://<relay-host>:3032 --code a1b2c3d4
Option B: Manual (if you already have shell access)
# On the target:
remotecmd-cli set-relay --url http://<relay-host>:3032 --name myserver
remotecmd-cli daemon start -daemon
# copy the printed token
# On your client:
remotecmd-cli set-relay --url http://<relay-host>:3032 --name myclient
remotecmd-cli add-target --name myserver --token <token># Single target (legacy syntax)
remotecmd-cli --target myserver --cmd 'df -h'
# Single target (new syntax)
remotecmd-cli exec --target myserver --cmd 'hostname'
# Streaming mode
remotecmd-cli exec --target myserver --cmd 'journalctl -f' --stream --timeout 60
# With aliases
rcx myserver 'uptime'
rcx myserver 'docker ps' 15This is where remotecmd-cli really shines. Run commands across your fleet in one shot.
remotecmd-cli exec --targets web1,web2,db1 --cmd 'uptime'# Create groups
remotecmd-cli group create prod-web --targets web1,web2,web3
remotecmd-cli group create prod-db --targets db1,db2
# Execute on a group
remotecmd-cli exec --group prod-web --cmd 'systemctl reload nginx'
# Execute on all prod machines (target can be in multiple groups)
remotecmd-cli exec --targets web1,web2,db1,db2 --cmd 'date'Table (default, for humans):
$ remotecmd-cli exec --targets web1,db1 --cmd 'hostname' --format table
TARGET | STATUS | OUTPUT/ERROR
---------------------|--------|----------------------------------------
web1 | OK | web1.example.com
db1 | OK | db1.internal
JSON (for agents and scripts):
{
"type": "multi_result",
"results": {
"web1": {"ok": true, "stdout": "web1.example.com", "exit_code": 0, "duration_ms": 5},
"db1": {"ok": true, "stdout": "db1.internal", "exit_code": 0, "duration_ms": 4}
}
}remotecmd-cli group create --name <n> --targets <t1,t2,...> # Create group
remotecmd-cli group add --name <n> --targets <t1,t2,...> # Add targets to group
remotecmd-cli group remove --name <n> --targets <t1,t2,...> # Remove targets from group
remotecmd-cli group delete --name <n> # Delete group
remotecmd-cli group list # List all groups
remotecmd-cli list-targets # List targets + groupsCopy files and directories to remote targets:
# Single file
remotecmd-cli cp --target myserver --src ./config.yaml --dst /etc/app/config.yaml
# Directory (auto-detected, uses tar archive + base64)
remotecmd-cli cp --target myserver --src ./dist --dst /var/www/app
# With streaming progress
remotecmd-cli cp --target myserver --src ./large-file --dst /tmp/large-file --stream
# Using the rcc alias
rcc myserver ~/.ssh/config ~/.ssh/config
rcc myserver /app/dist /app/dist --streamThe pair command is the fastest way to add a machine you don't yet have shell access to:
You (client) Remote machine
───────────────── ─────────────────────────────────────
remotecmd-cli pair (receives one-liner from you)
listen --name vps1
│ curl ... | sh -s -- --relay ... --code abc123
│ registers code "abc123" │
│ on relay │ installs binary
│ │ sets relay config
│ │ saves pair code
│ │ starts daemon
│ │
│◄──────── pair message ──────────────┘
│ (code + token + hostname)
│
adds target "vps1"
prints: Peer connected!
- Pair codes are one-time use
- Daemon auto-starts on boot via systemd user service (falls back to nohup)
Without --stream, output is buffered and returned as JSON after the command exits.
With --stream, stdout and stderr are forwarded line by line in real time:
# Watch a build in real time
remotecmd-cli exec --target myserver --cmd 'make all' --stream --timeout 300
# Follow logs
remotecmd-cli exec --target myserver --cmd 'journalctl -f -u nginx' --stream --timeout 3600JSONL Streaming (for agents):
rcx myserver 'long-cmd' --stream
# Output: {"event":"chunk","data":{"stream":"stdout","data":"line"}}
# {"event":"complete","data":{"ok":true,"exit_code":0,"duration":123}}EXECUTE (single):
remotecmd-cli --target <n> --cmd <cmd> [--timeout <s>] [--stream] Legacy syntax
remotecmd-cli exec --target <n> --cmd <cmd> [--timeout <s>] [--stream] New syntax
EXECUTE (multi-target):
remotecmd-cli exec --targets <t1,t2,...> --cmd <cmd> [--timeout <s>] [--format json|table]
remotecmd-cli exec --group <name> --cmd <cmd> [--timeout <s>] [--format json|table]
FILE TRANSFER:
remotecmd-cli cp --target <n> --src <path> --dst <path> [--stream]
GROUPS:
remotecmd-cli group create --name <n> --targets <t1,t2,...>
remotecmd-cli group add --name <n> --targets <t1,t2,...>
remotecmd-cli group remove --name <n> --targets <t1,t2,...>
remotecmd-cli group delete --name <n>
remotecmd-cli group list
PAIRING:
remotecmd-cli pair listen [--name <n>] [--timeout <s>] [--code <c>]
remotecmd-cli pair accept --code <c>
CONFIGURATION:
remotecmd-cli set-relay --url <u> --name <n>
remotecmd-cli add-target --name <n> --token <t>
remotecmd-cli remove-target --name <n>
remotecmd-cli list-targets
ALIASES:
remotecmd-cli alias install Install rc / rcx / rcl / rcs / rcc
remotecmd-cli alias uninstall Remove installed aliases
RELAY:
remotecmd-cli relay daemon start [--port 3032] [-daemon]
remotecmd-cli relay daemon stop
remotecmd-cli relay daemon status
DAEMON:
remotecmd-cli daemon start [-daemon]
remotecmd-cli daemon stop
remotecmd-cli daemon status
| Alias | Equivalent | Description |
|---|---|---|
rc |
remotecmd-cli |
Full CLI shortcut |
rcx <target> <cmd> [--stream] [timeout] |
exec --target <t> --cmd <c> |
Execute command (default 10s) |
rcl |
list-targets |
List configured targets + groups |
rcs <target> |
exec --target <t> --cmd 'status check' |
Check daemon status via PID file |
rcc <target> <src> <dst> [--stream] |
cp --target <t> --src <s> --dst <d> |
Copy files/directories |
Single-target (buffered):
{
"ok": true,
"stdout": "myserver\n",
"stderr": "",
"exit_code": 0,
"duration_ms": 8
}Multi-target (JSON):
{
"type": "multi_result",
"results": {
"web1": {"ok": true, "stdout": "OK", "exit_code": 0, "duration_ms": 5},
"web2": {"ok": false, "error": "target not connected"}
}
}┌────────────┐ WebSocket ┌─────────────┐ WebSocket ┌──────────────────┐
│ Client │──────────────►│ Relay Hub │◄──────────────│ Target Daemon 1 │
│ (one-shot) │ │ (always on)│ │ (persistent bg) │
└────────────┘ │ │ └──────────────────┘
│ │ ┌──────────────────┐
│ │◄──────────────│ Target Daemon 2 │
│ │ └──────────────────┘
│ │ ┌──────────────────┐
│ │◄──────────────│ Target Daemon N │
│ │ └──────────────────┘
└─────────────┘
│
routes by target name
+ token verification
+ multi-target fan-out
Relay — stateless hub. Routes single commands by target name. For multi-target, fans out to N daemons, collects results, and returns an aggregated response.
Daemon — runs on each target. Connects out to relay (no inbound ports). Forks a shell on command, returns structured JSON.
Client — one-shot WebSocket connection. Sends command, waits for result.
| Scenario | Command |
|---|---|
| Quick health check | rcx myserver 'uptime && df -h' |
| Fleet health check | remotecmd-cli exec --group all --cmd 'uptime' --format table |
| Rolling restart | remotecmd-cli exec --group web --cmd 'systemctl restart nginx' |
| Deploy to remote | remotecmd-cli exec --target app1 --cmd 'cd /app && git pull && pm2 restart all' 60 |
| Stream build logs | remotecmd-cli exec --target build --cmd 'make' --stream --timeout 300 |
| Follow app logs | remotecmd-cli exec --target prod --cmd 'tail -f /var/log/app.log' --stream |
| Copy config to all | for t in web1 web2 web3; do rcc "$t" ./app.conf /etc/app/app.conf; done |
| Add friend's machine | remotecmd-cli pair listen --name friend → share one-liner |
| Symptom | Cause | Fix |
|---|---|---|
target not connected |
Daemon not running or wrong relay URL | Check daemon status on target; verify relay URL matches |
pair code not found |
Code already used or listener timed out | Run pair listen again for a fresh code |
curl: (23) Failure writing output |
Binary busy (systemd running it) | install.sh stops service first; re-run one-liner |
| Token mismatch | Target re-started with new token | Re-add target: add-target --name <n> --token <new> |
| Streaming stops early | Default timeout hit | Add --timeout <seconds> |
| Group target not resolved | Target not in config | Add target first with add-target, then add to group |
| Layer | Technology |
|---|---|
| Language | Go — single static binary, no runtime deps |
| Transport | WebSocket (gorilla/websocket) |
| Auth | Token per target (auto-generated) |
| Persistence | ~/.remotecmd/config.json |
| Daemon | PID file + nohup (fallback) |
| Streaming | StdoutPipe + bufio.Scanner, line-by-line forwarding |
| Multi-target | Relay-level fan-out with result aggregation |
| Releases | GitHub Actions → multi-arch binaries (linux/darwin, amd64/arm64) |
See docs/vision.md for the full vision and roadmap.
Upcoming priorities:
- v1.3: Script-friendly exit codes, systemd unit generation
- v1.4: Persistent client connections for faster sequential commands
- v1.5: Optional TLS for relay encryption
MIT — Javier Leandro Arancibia
