ssh-cli
- Historical note: 0.4.2 closed TUN-003 / IO-010; 0.5.0 was the EN/API rename + secrets force-init reencrypt; current product line is 0.5.2 (export/import agent roundtrip, wire schema v3 dual-read, secrets CLI flags, tunnel
--bind).
Give any LLM remote SSH power in one memory-safe one-shot binary.
- Read this document in Portuguese (pt-BR).
- Install with
cargo install ssh-cli --lockedfor a lockfile-aligned graph. - Upgrade with
cargo install ssh-cli --locked --force. - Verify with
ssh-cli --version. - Read full history in CHANGELOG.md.
- Integrate agents via docs/AGENTS.md and INTEGRATIONS.md.
- Follow first use in docs/HOW_TO_USE.md.
- Copy recipes from docs/COOKBOOK.md.
- Check platforms in docs/CROSS_PLATFORM.md.
- Migrate from 0.3.3+ in docs/MIGRATION.md (target line 0.5.2).
- Run tests via docs/TESTING.md.
- Consume JSON contracts under docs/schemas/README.md.
- Teach LLMs with skills/ssh-cli-en/SKILL.md.
What is it?
One-shot multi-host SSH CLI for agents
- Ship a single Rust binary with zero Node runtime and zero daemon.
- Operate N VPS hosts from XDG storage without
.envfiles. - Authenticate with password or private key per host.
- Execute
exec,sudo-exec, andsu-execas pure one-shot processes. - Capture stdout and stderr with structured JSON for orchestration.
- Auto-detect locale between
en-USandpt-BR. - Disable telemetry completely in every build.
Why ssh-cli?
Replace long-lived SSH processes with a die-after-run binary
- Avoid resident Node processes that hold sockets open between tasks.
- Cut RAM and CPU waste from long-lived SSH sessions.
- Register multi-host credentials once under XDG with atomic writes.
- Align command packing and dual maxChars semantics with the one-shot agent contract.
- Trust host keys via TOFU known_hosts instead of always-trust.
- Route errors with sysexits codes that agents classify reliably.
Superpowers
Capabilities that make agents productive
- Multi-host CRUD with
vps add|list|show|edit|remove|path|doctor|export|import - One-shot remote execution with
exec,sudo-exec,su-exec - Safe
sudopacking viash -cand shell escape - Private key auth with optional passphrase
- Dual limits
max_command_charsandmax_output_chars - Timeout with best-effort remote abort
- Bounded tunnel via mandatory
--timeout-ms; optional--bind(default127.0.0.1) - Wire schema v3: serialize English TOML keys; dual-read EN + legacy PT aliases on load
vps exportbody is TOML by default (TTY and pipe); agent JSON envelope only with--json; redacted by default; empty secrets stay""; non-empty redacted secrets mask as***(FIXED_MASK);--include-secretsto pipe/non-TTY needs-o/--outputor--i-understand-secrets-on-stdoutvps importaccepts TOML (EN keys + PT aliases) or JSONvps-exportenvelopes; redacted skeletons need--allow-incomplete- SCP upload and download of regular files only (no recursive directories on SCP; first solid wire fix in 0.4.0; prefer 0.5.2+ — avoid crates.io 0.3.9 SCP)
- SFTP subsystem (
ssh-cli sftp): upload/download (optional--recursivetrees, symlink no-follow),ls/mkdir/rmdir/rm/stat/rename; JSON eventssftp-transfer/sftp-list/sftp-fs-op/sftp-batch - SCP flag parity with exec:
--timeout,--password-stdin,--key,--key-passphrase/--key-passphrase-stdin,--json(contractdocs/schemas/scp-transfer.schema.json) - SCP download writes
{path}.ssh-cli.partialthen atomic rename; preserve remote mtime/mode bi-directional; upload streams 32 KiB chunks - SCP JSON success requires
event: "scp-transfer"(0.4.1 IO-009); missing remote →file not found: <path>exit 66 tunnel --jsonemits structuredtunnel_listeningafter local bind- Tunnel post-bind deadline exits 0 after
tunnel_listening(pre-bind timeout still 74) (0.4.1 TUN-002) - Tunnel and health-check auth parity with exec/scp:
--password-stdin,--key,--key-passphrase/--key-passphrase-stdinas applicable (0.4.1 CLI-005/006) - Redacted
vps exportnever emitssshcli-enc:for empty password — empty secrets serialize as empty strings (0.4.1 EXP-001) - Health-check latency probe with optional
--timeout - Shell completions for bash zsh fish powershell
- Secrets via stdin flags to avoid argv leaks
- Default at-rest encryption (ChaCha20-Poly1305) with auto XDG
secrets.key; CLI flags--allow-plaintext-secrets,--secrets-key-file,--use-keyringpreferred over env - Master-key UX:
secrets status|init|reencryptwith--jsoneventssecrets-init/secrets-reencrypt; first secret write foldssecrets_key_auto_created: trueinto the samevps-addedJSON (one document) - TOFU
known_hostsand atomic config writes with flock - Key-only hosts: empty password serializes as JSON
null(not"***") invps list/show - Default tracing filter is
error(agent-first clean stderr); use-vfor debug (ambientRUST_LOGis ignored) - Install with russh 0.62.2 for clean
cargo install --locked
Quick Start
Install and run the first remote command
|
Installation
Choose the install path that matches your environment
- Prefer crates.io with lockfile:
cargo install ssh-cli --locked(0.5.2+ on crates.io; avoid 0.3.9 for SCP). - Rebuild from a checkout:
cargo install --path . --locked - Do not use install without
--lockedunless you verified the crypto pins resolve cleanly. - Force upgrade after a release:
cargo install ssh-cli --locked --force - Build musl with allocator feature when targeting Alpine:
--features musl-allocator - Require Rust MSRV 1.85.0 or newer
Features
Cargo feature flags
| Feature | Default | Description |
|---|---|---|
ssh-real |
yes | Real SSH via russh + aws-lc-rs (compression none only) |
tls |
yes | rustls ≥0.23.18 + aws_lc_rs: SSH-over-TLS, mTLS, ACME |
musl-allocator |
no | mimalloc as global allocator (binary; useful on musl/Alpine) |
- Install path always enables defaults (
ssh-real+tls). - Disable real SSH / TLS only for dependency diagnosis:
cargo build --no-default-features. - docs.rs builds with
all-features = trueand--cfg docsrs(seeCargo.toml[package.metadata.docs.rs]).
Crypto policy (G-TLS)
- Default transport is SSH-2 on plain TCP (
russh+ aws-lc-rs). Host keys use TOFU under XDG (known_hosts). - Optional SSH-over-TLS (
vps add --tls, record fieldstls/tls_sni/ mTLS paths): rustls handshake then SSH on the TLS stream. Binary installsCryptoProvider(aws_lc_rs) once inmain. - mTLS / ACME:
ssh-cli tls mtls …andssh-cli tls acme …store material under XDGtls/(no product env for cert storage). - No OpenSSL /
native-tls/ dualringprovider. SSH channel compression forced tonone. - ACME validation /
invalidContact/ 4xx problem types → exit 64 non-retryable (G-E2E-01); rate limits remain transient exit 74. - Details: SECURITY.md.
Targets
Platforms covered by docs.rs metadata
x86_64-unknown-linux-gnu(default)x86_64-apple-darwin,aarch64-apple-darwinx86_64-pc-windows-msvcaarch64-unknown-linux-musl- See docs/CROSS_PLATFORM.md for runtime notes.
Usage
Register hosts then execute one-shot commands
- Default at-rest encryption (ChaCha20-Poly1305): auto
secrets.keyon first secret write; prefer CLI flags--secrets-key-file,--use-keyring,--allow-plaintext-secrets(or XDGsecrets.key); manage withssh-cli secrets status|init|reencrypt.SSH_CLI_SECRETS_KEY/SSH_CLI_SECRETS_KEY_FILEare rejected fail-closed (not a store). Opt-out for tests only via--allow-plaintext-secrets. - Prefer
--password-stdin/--keyover argv secrets. - Add password hosts with
vps add --passwordor--password-stdin. - Add key hosts with
vps add --key ~/.ssh/id_ed25519. - On key-only hosts, empty password fields serialize as JSON
nullinvps list/show(non-empty secrets mask as"***"). - Mark active host with
connect <name>. - Run remote shells with
exec <vps> "<cmd>". - Elevate with
sudo-execorsu-execwhen configured. - Diagnose XDG paths with
doctor --json(orvps doctor --json). - Discover contracts with
ssh-cli schema/ssh-cli commands. - Export redacted inventory with
vps export(TOML default;--jsonfor agent envelope).
Commands
Product surface for humans and agents
| Command | Purpose |
|---|---|
ssh-cli vps add |
Register a host (password or key or --use-agent / --agent-socket) |
ssh-cli vps list [--json] |
List hosts with secrets masked |
ssh-cli vps show <name> [--json] |
Show one host masked |
ssh-cli vps edit <name> |
Patch host fields |
ssh-cli vps remove <name> |
Delete host |
ssh-cli vps path |
Print config.toml path |
ssh-cli vps doctor [--json] |
Show XDG layer schema and paths |
ssh-cli doctor [--json] |
Root alias of vps doctor (G-E2E-03) |
ssh-cli schema [NAME] |
Emit embedded JSON Schema catalog or one schema body (G-E2E-02) |
ssh-cli commands |
Emit full command tree as JSON (agent discovery) |
ssh-cli vps export |
Export hosts as TOML by default (TTY and pipe); JSON agent envelope only with --json; secrets redacted by default; empty secrets stay "" (never fake sshcli-enc:); --include-secrets to pipe/non-TTY requires -o/--output or --i-understand-secrets-on-stdout |
ssh-cli vps import --file |
Import hosts from TOML (EN keys + PT aliases) or JSON vps-export envelope; redacted skeletons need --allow-incomplete |
ssh-cli connect <name> |
Write sibling active file |
ssh-cli exec <vps> <cmd> |
One-shot remote command |
ssh-cli exec --all '<cmd>' |
Bounded concurrent remote command on every registered host (exec-batch JSON) |
ssh-cli sudo-exec <vps> <cmd> / --all |
One-shot sudo with safe packing (fleet with --all) |
ssh-cli su-exec <vps> <cmd> / --all |
One-shot su - elevation (fleet with --all) |
| `ssh-cli scp upload | download` |
ssh-cli sftp upload|download|ls|mkdir|rmdir|rm|stat|rename |
SFTP v3 subsystem; --recursive trees (no symlink follow); auth parity with scp; JSON sftp-transfer / sftp-list / sftp-fs-op / sftp-batch |
ssh-cli tunnel ... --timeout-ms N [--bind ADDR] [--json] |
Bounded local port forward; --bind default 127.0.0.1; auth --password-stdin / --key / --key-passphrase[-stdin]; --json emits tunnel_listening after bind; post-bind deadline exits 0 (pre-bind timeout still 74); concurrent accepts gated by --max-concurrency |
ssh-cli health-check [<vps>] [--timeout N] / --all |
Connectivity probe; optional --timeout ms; auth --password-stdin / --key / --key-passphrase[-stdin]; --all → fleet probe (health-check-batch) |
ssh-cli --max-concurrency N … |
Global cap (1..=64) for multi-host fan-out and tunnel forwards (auto CPUs×RAM formula when omitted) |
| `ssh-cli secrets status | init |
ssh-cli completions <shell> |
Shell completion scripts |
Configuration (CLI-only product store)
Product knobs are flags and XDG — not SSH_CLI_* env stores
| Control | How | Example |
|---|---|---|
| Config directory | --config-dir (else XDG/directories) |
ssh-cli --config-dir /tmp/ssh-cli-test vps list |
| Language | --lang or ssh-cli locale set <code> (XDG lang) |
ssh-cli --lang pt-BR … |
| Output format | --json / --output-format json|text |
ssh-cli exec h uptime --json |
| Concurrency | --max-concurrency N (1..=64; auto formula when omitted) |
ssh-cli --max-concurrency 8 exec --all id --json |
| Primary-key | --secrets-key-file, --use-keyring, or XDG secrets.key |
ssh-cli --secrets-key-file ./k secrets status |
| Plaintext opt-out | --allow-plaintext-secrets (tests only) |
ssh-cli --allow-plaintext-secrets … |
Fail-closed secrets env (not a store)
| Variable | Behavior |
|---|---|
SSH_CLI_SECRETS_KEY / SSH_CLI_SECRETS_KEY_FILE |
Rejected if present — use XDG secrets.key, --secrets-key-file, or --use-keyring |
OS / host boundary (detection only — not product config store)
| Variable | Role |
|---|---|
HOME |
OS home for XDG path resolution |
TERM / NO_COLOR / CLICOLOR_FORCE |
Terminal / color capability |
CI / Flatpak-related markers |
Runtime detection (vps doctor runtime.*) |
RUST_LOG |
Ignored by product (not a config store); use -v for debug |
- Default tracing filter is
errorso agent stderr stays clean; pass-vfor debug (ambientRUST_LOGis ignored). - Never put host passwords in environment variables; use registry + stdin.
- Product does not read
SSH_CLI_HOME,SSH_CLI_LANG,SSH_CLI_FORCE_TEXT, orSSH_CLI_MAX_CONCURRENCYas config stores.
Integration Patterns
Wire agents with one-shot subprocesses only
- Invoke
ssh-clias a subprocess with explicit argv. - Prefer
--jsonor--output-format jsonfor machine parsing. - Parse stdout only; default log level is
errorso stderr stays silent for JSON pipelines — pass-vwhen diagnosing (ambientRUST_LOGis ignored). - Map non-zero exits with sysexits semantics before retry.
- Store hosts once via
vps addthen callexecper task. - Pass secrets through
--password-stdinwhen argv history is risky. - Read INTEGRATIONS.md for agent-specific notes.
Exit Codes
Sysexits-style codes agents must map before retry
| Code | Meaning |
|---|---|
0 |
Success |
1 |
General runtime error |
64 |
Usage / invalid arguments |
65 |
Data error (JSON/TOML/schema) |
66 |
VPS or input file not found |
73 |
Cannot create config/output |
74 |
IO or SSH connection/timeout |
77 |
Authentication rejected or host-key / sudo policy |
130 |
SIGINT |
143 |
SIGTERM |
- Prefer
--jsonor auto JSON when stdout is not a TTY (--output-formatoverrides). - Default tracing is
error, so exit handling and JSON stdout stay free of INFO noise; use-vonly when diagnosing (ambientRUST_LOGis ignored). - Retry only on transient IO/timeout (
74), never on auth (77) or usage (64).
Performance
Cold start and memory goals
- Target cold start under 100 ms on modern Linux hosts.
- Keep process memory far below a resident long-lived Node SSH process.
- Die after each command so RAM returns to the OS immediately.
- Avoid long-lived tunnels without
--timeout-ms.
Memory Requirements
Plan capacity for multi-host registries
- Config TOML size grows with host count and path lengths.
- Output buffers respect
max_output_charsper stream. - Known_hosts file grows slowly with unique host:port pairs.
- No embedding model and no Node heap are required.
Troubleshooting FAQ
Fix common install and runtime failures
- Install fails on crypto RC drift: rerun with
--lockedor use release 0.5.2+ (russh 0.62.2) (scripts/verify_install_resolve.sh). - Auth fails on key-only hosts: set
--keyonvps addor pass--key/--password-stdintoexec(rejected auth exits 77). - Auth fails with passphrase keys: use
--key-passphrase-stdin(exit 77 on reject). - Host key changed: confirm legitimacy then rerun with
--replace-host-key. - Command rejected as too long: raise
max_command_charsor shorten the command. - Config has encrypted secrets but no key: run
ssh-cli secrets initor restoresecrets.key/ env master-key /--secrets-key-file. - sudo-exec disabled: remove
--disable-sudoand setdisable_sudo=falseon the host. - Unexpected stderr noise in JSON pipelines: default log level is already
error; pass-vwhen diagnosing (ambientRUST_LOGis ignored). - SCP from crates.io 0.3.9 fails or writes 0-byte remotes: upgrade to 0.5.2+ (wire fix since 0.4.0); only regular files, not directories.
- SCP remote missing: message is
file not found: <path>and exit 66 (prefer 0.5.2+; 0.4.2 IO-010). - SCP download fails mid-transfer: destination stays absent or previous file intact (partial uses
.ssh-cli.partial). - Redacted
vps exporton 0.4.0 wrote fakesshcli-enc:blobs for empty secrets: upgrade to 0.5.2+ (empty secrets stay empty strings since 0.4.1). - Tunnel emitted
ok: true/tunnel_listeningthen process exit 74 when the post-bind deadline hit on 0.4.0: upgrade to 0.5.2+ (post-bind deadline exits 0 since 0.4.1; pre-bind timeout still 74). - Import bad TOML: parse errors map to exit 65 (
TomlDe/ data error). - Import redacted skeleton without secrets: pass
--allow-incomplete. - macOS Gatekeeper blocks binary: run
xattr -d com.apple.quarantine /path/to/ssh-cli. - Permission denied on config: ensure
chmod 600on the XDGconfig.tomlandsecrets.key.
Contributing
- Read CONTRIBUTING.md before opening a pull request.
- Follow the bilingual documentation framework for every public doc change.
Security
- Read SECURITY.md for private vulnerability reporting.
- Prefer stdin secret flags and key files over argv passwords.
Changelog
- Read version history in CHANGELOG.md.
- Do not paste release notes into this README.
License
- Dual-licensed under MIT or Apache-2.0.
- See LICENSE, LICENSE-MIT, and LICENSE-APACHE.