Give any large language model (LLM) remote SSH power through one memory-safe one-shot command-line interface (CLI) binary.
- Read this document in Portuguese (pt-BR)
- Install with
cargo install ssh-cli --lockedand verify withssh-cli --version - Current version is
0.5.6with minimum supported Rust version (MSRV)1.89 - Map the product for LLMs in llms.txt and llms-full.txt
- Jump to Quick Start, Documentation or the Cookbook
- Ship a single Rust binary with zero Node runtime and zero daemon
- Operate N VPS hosts from XDG storage without
.envfiles - Authenticate per host with a password, a private key or the SSH agent
- Run
exec,sudo-execandsu-execas pure one-shot processes that die after the command - Return structured JSON on stdout for orchestration, with execution failures in a stable envelope on stderr
- Collect no telemetry in any build
- Avoid resident processes that hold sockets and RAM between tasks
- Register multi-host credentials once, with at-rest encryption and atomic writes
- Trust host keys through trust on first use (TOFU)
known_hostsinstead of always-trust - Route errors with sysexits codes that agents classify reliably
russh0.65.1 closes RUSTSEC-2026-0492, RUSTSEC-2026-0495 and two GHSA advisories, and the MSRV is now Rust 1.89 because russh 0.65 requires itsecrets init --keyringwrites to the operating system store, and an unavailable store exits 69 instead of reporting success--selectand--truncate-contentnow reduce a single record, socommands --select tree.aboutreturns one field- A reduced bare array writes one
{"event":"agent-shape",...}line to stderr, and--quietsuppresses it - A collection flag passed on a single record is named in
agent_shape.ignoredinstead of being dropped in silence - Every one of the 48 leaves shows an
Examples:block in--help - The TLS root store no longer carries a distrusted authority, and a new gate compares the advisory database with the resolved tree
- Rename a host natively with
vps renameorvps edit --name, keeping every credential - Switch the authentication method of a host in one
vps editcall - Host names follow a strict ASCII rule, and legacy names still load and can be repaired
- Text mode escapes terminal control sequences
- A new config directory is born
0700, andvps doctorreportsconfig_dir_exposed,config_dir_permissionsandinvalid_host_names --dry-runcoversvps editandvps renameand echoes the target--lock-wait-msbounds the config lock wait and exits 69- Read the full list in CHANGELOG.md
- Run
exec,sudo-execandsu-execone-shot, with safesudopacking viash -cand shell escape - Name the target on purpose with
<VPS> <COMMAND>, a selector (--all,--hosts a,b,--tags prod) or--use-active - A lone positional exits 64, so a host name is never run as a remote binary
- Verify the target afterwards with
target_resolved,target_sourceandactive_fallbackin the single-host envelope - Read
host_resolvedandhost_sourceas the read aliases of the same fields - Add extra commands on the same SSH session with
--step exec --jsonwith one step emits exactly one NDJSON success object- Apply the dual limits
max_command_charsandmax_output_chars - Abort on timeout with a best-effort remote abort
- The remote stdout and stderr of
execstay raw by design
- Manage hosts with
vps add,vps list,vps show,vps edit,vps remove,vps rename,vps path,vps doctor,vps exportandvps import - Rename natively with
vps rename <NAME> <NEW_NAME>orvps edit <NAME> --name <NEW>, and every credential stays readable - The rename emits
vps-renamedwithprevious_nameandactive_rewritten - Switch the authentication method in one call:
vps edit --keyon a password host,--password-stdinon a key host, or--use-agent - Each method clears the other ones, and
updatedlists the new method plus only the fields that had a value - Two primary auth flags in one call exit 2, and a passphrase without a key exits 64 with the record untouched
- A different
--keydrops the old passphrase unless the call brings a new one --agent-socketalone implies--use-agent- Remove a field with the eight clear flags
--no-key,--no-agent-socket,--clear-sudo-password,--clear-su-password,--no-tls-sni,--no-tls-client-cert,--no-tls-client-keyand--clear-tags - Accept host names only in ASCII
[A-Za-z0-9._-], starting with a letter or digit, from 1 to 255 bytes - The strict rule applies on
vps add, on the destination ofvps renameand onvps import vps importvalidates every item, lists all invalid names at once, exits 64 and writes nothing, also under--dry-run- A legacy name with control or bidi characters still loads, and
vps rename '<legacy name>' <new>repairs it - Two
config.tomltables that normalize to the same NFC name exit 65 withconfig has two hosts named ... vps add --disable-sudoandvps edit --disable-sudopersist in the host record,vps edit --enable-sudoundoes it, and--disableSudois refused on those two verbs- Wait for the config lock up to
--lock-wait-ms(default 5000 ms, maximum 300000) and then exit 69 instead of hanging - Store wire schema v3 with English TOML keys, and read legacy PT aliases on load
- Preview eight operations:
vps remove,vps import,vps edit,vps rename,sftp rm,sftp rmdir,secrets init,secrets reencrypt - Any other command with
--dry-runexits 64 instead of ignoring the flag - The plan of
vps edit,vps renameandvps removeechoestarget_resolved,target_source,host_resolvedandhost_source - The three verbs exit 66 on a missing config without creating a directory or a lock
- Text mode escapes ESC, BEL, bidi and format characters as
\u{1b}, in messages, errors,vps list,vps show,health-check,vps doctorand batch output sftp lsandsftp statalso escape\nand\tin remote names, so a hostile name cannot forge a listing line- JSON keeps the original value, because it is for machines
- Shape the payload before serialization with
--select,--filter,--limit,--sort,--dedupe-by,--count-only,--truncate-contentand--max-output-bytes - The collection flags reduce the
vps listarray, theresultsof batch envelopes and theentriesofsftp ls - A single object such as
vps show, a one-hostexecorvps doctoris reduced by--selectand--truncate-content, sovps show prod --select namereturns one field - The collection flags (
--filter,--limit,--sort,--dedupe-by,--count-only,--max-output-bytes) cannot apply to one record, and they are named underagent_shape.ignoredinstead of being silently dropped - A reduced envelope gains
agent_shapewithinput_count,output_countanddropped, and a reduced single record gainsselected,content_truncatedandignored - A reduced bare array keeps its type and reports the drop on stderr as
{"event":"agent-shape",…}, which--quietsuppresses - Default tracing filter is
error, which keeps stderr clean - Graduated verbosity
-v,-vvand-vvvis always crate-scoped (warn,ssh_cli=*), so russh never dumps passwords - Ambient
RUST_LOGis ignored
- A new config directory is born
0700, andvps doctorreportsconfig_dir_permissions,config_dir_exposedandinvalid_host_namesunderlocal - An existing default XDG directory is tightened to
0700when it is not a symlink and belongs to you - Any other existing directory is never chmodded: it logs a warning (visible with
-v) and shows up indoctor --config-dir algo.tomluses the parent of the.tomlpath as the config directory for every file- The directories created by
vps export -oare born0700, andknown_hosts.lockandlangare born0600 tls mtls import --namerefuses control, format and bidi characters with exit 64 and creates no directory, while Unicode such asserviço-1stays valid
- Secure Copy Protocol (SCP) uploads and downloads regular files only, with no recursive directories
- SCP flags follow
exec:--timeout,--password-stdin,--key,--key-passphrase,--key-passphrase-stdin,--json(contractdocs/schemas/scp-transfer.schema.json) - SCP JSON success carries
event: "scp-transfer", and a missing remote file exits 66 withfile not found: <path> - An SCP download writes
{path}.ssh-cli.partialand then renames atomically, and a failedsync_datastops the rename - Upload streams 32 KiB chunks
- Remote mtime and mode preservation is best-effort and reported as
mtime_preservedanddurable - SSH File Transfer Protocol (SFTP) uses the subsystem:
sftp upload,sftp download(optional--recursive, symlinks never followed),sftp ls,sftp mkdir,sftp rmdir,sftp rm,sftp stat,sftp rename - SFTP JSON events are
sftp-transfer,sftp-list,sftp-fs-opandsftp-batch - SFTP upload preserves payload bytes and never truncates to zero
SFTP_PERM_MASK(0o7777) applies on upload, andSFTP_PERM_MASK_UNTRUSTED(0o0777) strips setuid bits that a server sends on download- Batch cancel pads the cancelled remainder, so the results length matches the input
- Open a bounded tunnel with the mandatory
--timeout-msand an optional--bind(default127.0.0.1) - A non-loopback
--bindrequires--i-accept-network-exposure tunnel --jsonemitstunnel_listeningafter the local bind andtunnel_closedat the end- A post-bind deadline exits 0, while a pre-bind timeout exits 74
- A SIGINT exits 130 and a SIGTERM exits 143, and after the bind
tunnel_closedcarriesreason: signal - Use
--socks5for a SOCKS5 proxy,--remote-socket PATHfor a Unix socket on the remote host and--reversefor a server-side listener - Probe latency with
health-checkand an optional--timeout, for one host or the fleet with--all
- Encrypt secrets at rest by default with ChaCha20-Poly1305 and an automatic XDG
secrets.key - Choose the key with
--secrets-key-file,--use-keyringor XDGsecrets.key, and opt out only for tests with--allow-plaintext-secrets - Manage the master key with
secrets status,secrets initandsecrets reencrypt, with eventssecrets-initandsecrets-reencrypt - The first secret write folds
secrets_key_auto_created: trueinto the samevps-addedJSON - Pass secrets through stdin flags to keep them out of argv
- Keep TOFU
known_hostsand atomic config writes under flock - Use SSH-over-TLS, mutual TLS (mTLS) identities and Automatic Certificate Management Environment (ACME) DNS-01 certificates through
ssh-cli tls
vps exportbody follows the resolved output format: JSON whenever stdout is not a TTY, even when-onames a.tomlfile- A TOML body requires
--output-format text - Export is redacted by default: a password comes out as an empty string
""and absent secrets asnull, never as ciphertext - The fixed three-asterisk mask
FIXED_MASKexists only invps listandvps show --include-secretsinto a pipe or non-TTY needs-oor--i-understand-secrets-on-stdout- A key-only host shows an empty password as JSON
nullinvps listandvps show vps importaccepts TOML (EN keys plus PT aliases) or a JSONvps-exportenvelope, and redacted skeletons need--allow-incomplete- A host exported with a legacy name outside the strict charset restores only after
vps rename
- Print every contract with
ssh-cli schema(25 embedded JSON Schemas) and the full command tree withssh-cli commands - Complete the shell with
ssh-cli completionsfor bash, zsh, fish, powershell and elvish - Pick the UI language between
en-USandpt-BRautomatically
cargo install ssh-cli --locked
printf %s 'demo-password-not-real' | ssh-cli vps add --name prod --host prod.example.com --port 22 --user admin --password-stdin
ssh-cli exec prod "hostname" --json{"ok":true,"event":"vps-added","key_file":"~/.config/ssh-cli/secrets.key","key_source":"xdg_file","name":"prod","secrets_key_auto_created":true}- Prefer crates.io with the lockfile:
cargo install ssh-cli --locked - Upgrade after a release with
cargo install ssh-cli --locked --force - Rebuild from a checkout with
cargo install --path . --locked - Do not install without
--lockedunless you verified that the crypto pins resolve cleanly - Build musl with the allocator feature on Alpine:
--features musl-allocator - Require Rust
1.89or newer (MSRV, declared asrust-versioninCargo.toml)
- Bash:
ssh-cli completions bash > ssh-cli.bashand source the file from your profile - Zsh:
ssh-cli completions zsh > _ssh-cliand place it in a directory of yourfpath - Fish:
ssh-cli completions fish > ~/.config/fish/completions/ssh-cli.fish - PowerShell:
ssh-cli completions powershell > ssh-cli.ps1and dot-source it from your profile - Elvish:
ssh-cli completions elvish > ssh-cli.elv
- The UI speaks
enandpt-BR - Resolution order is
--lang, then the XDGlangfile written byssh-cli locale set <code>, then the system locale, thenen - Error bodies stay in English by design, so an agent receives stable text
| Feature | Default | Description |
|---|---|---|
ssh-real |
yes | Real SSH via russh + aws-lc-rs (compression none only) |
tls |
yes | rustls + aws_lc_rs: SSH-over-TLS, mTLS, ACME, where Cargo.toml asks for 0.23.25 and the security floor that tls provider prints as min_rustls is 0.23.18 |
musl-allocator |
no | mimalloc as global allocator (binary, useful on musl and Alpine) |
- The install path always enables the defaults (
ssh-realandtls) - Disable real SSH and TLS only for dependency diagnosis:
cargo build --no-default-features - docs.rs builds with
all-features = trueand--cfg docsrs(see[package.metadata.docs.rs]inCargo.toml)
- 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) runs a rustls handshake and then SSH on the TLS stream - The binary installs the
aws_lc_rsCryptoProvideronce inmain - mTLS and ACME material lives under XDG
tls/, with no product env for certificate storage - No OpenSSL, no
native-tlsand no dualringprovider - SSH channel compression is forced to
none - ACME validation errors,
invalidContactand 4xx problem types exit 64 and are not retryable, while rate limits stay transient with exit 74 - Details in SECURITY.md
x86_64-unknown-linux-gnu(default)x86_64-apple-darwinandaarch64-apple-darwinx86_64-pc-windows-msvcaarch64-unknown-linux-musl- Windows and macOS pass a type-check only, and the
0700mode andgetuidwere not executed there - Windows has no chmod, so the config directory depends on the profile ACL
- See docs/CROSS_PLATFORM.md for runtime notes
- Prefer
--password-stdinor--keyover secrets in argv - Add a password host with
vps add --password-stdin(or--password) - Add a key host with
vps add --key ~/.ssh/id_ed25519 - Name hosts with ASCII letters, digits,
.,_and-, starting with a letter or digit - Mark the active host with
connect <name>, which only writes a marker and opens no session
- Switch from password to key in one call with
vps edit prod --key ~/.ssh/id_ed25519 - Switch from key to password with
vps edit prod --password-stdin - Rename with
vps rename prod prod-old, where a repeat after success exits 66 andvps show prod-oldconfirms - Preview any of these with
--dry-runand readtarget_resolvedbefore the real call - Repair a legacy name with
vps rename '<legacy name>' <new>
- Run a remote shell command with
exec <vps> "<cmd>" - Reach the whole fleet with
exec --all '<cmd>', a subset with--hosts a,bor a label with--tags prod - Reach the
connectmarker only on purpose withexec --use-active '<cmd>', which exits 66 when no marker exists - Elevate with
sudo-execorsu-execwhen configured - Disable elevation for one call with
--disable-sudo, or re-enable it on a host withvps edit --enable-sudo
- Diagnose paths and the local state with
doctor --json(orvps doctor --json) - Read
local.config_dir_exposed,local.config_dir_permissions,local.invalid_host_namesandlocal.active_vpsfrom that object - Probe hosts over SSH with
vps doctor --probe-sshand a--hostssubset - Discover contracts with
ssh-cli schemaandssh-cli commands - Export the inventory with
vps export, which writes a JSON envelope on any non-TTY stdout
- Auth set:
--password,--password-stdin,--key,--key-passphrase,--key-passphrase-stdin,--use-agent,--agent-socket - Fleet selectors:
--all,--hosts <LIST>, plus--tags <LIST>and--use-activewhere the table shows them - Every row lists the leaf with its local flags, and the global flags are in the next sections
| Group | Purpose | Leaves |
|---|---|---|
ssh-cli vps |
Manage registered VPS hosts | add, list, remove, rename, edit, show, path, doctor, export, import |
ssh-cli scp |
SCP file transfer | upload, download |
ssh-cli sftp |
SFTP transfer and remote filesystem operations | upload, download, ls, mkdir, rmdir, rm, stat, rename |
ssh-cli secrets |
Manage the primary key and at-rest secret encryption | status, init, reencrypt |
ssh-cli locale |
Diagnose and manage the UI language | show, set, clear |
ssh-cli tls |
TLS stack: provider status, mTLS identities and ACME certificates | provider, paths, mtls, acme |
ssh-cli tls mtls |
Manage imported mTLS client identities under XDG tls/mtls/ |
list, import, show, remove |
ssh-cli tls acme |
ACME account and DNS-01 certificate lifecycle | account, issue, complete, status, list |
ssh-cli tls acme account |
Manage the ACME account | create, show |
| Command | Purpose | Local flags |
|---|---|---|
ssh-cli vps add |
Register a host | --name, --host, --port, --user, auth set, --timeout, --max-command-chars, --max-output-chars, --max-chars (legacy alias of --max-command-chars), --sudo-password, --sudo-password-stdin, --su-password, --su-password-stdin, --disable-sudo, --tag, --tls, --tls-sni, --tls-client-cert, --tls-client-key, --check |
ssh-cli vps list |
List hosts with secrets masked | --tag |
ssh-cli vps show |
Show one host masked | <NAME> |
ssh-cli vps edit |
Patch fields, switch the auth method and rename | <NAME>, --name, --host, --port, --user, auth set, --timeout, --max-command-chars, --max-output-chars, --max-chars, --sudo-password, --sudo-password-stdin, --su-password, --su-password-stdin, --disable-sudo, --enable-sudo, --tls, --no-tls, --tls-sni, --tls-client-cert, --tls-client-key, --tag, --clear-tags, --no-key, --no-agent-socket, --clear-sudo-password, --clear-su-password, --no-tls-sni, --no-tls-client-cert, --no-tls-client-key |
ssh-cli vps remove |
Delete a host | <NAME> |
ssh-cli vps rename |
Rename a host and keep every credential | <NAME> <NEW_NAME> |
ssh-cli vps path |
Print the config.toml path |
none |
ssh-cli vps doctor |
Show XDG layers, schema, paths and local state | --probe-ssh, --hosts |
ssh-cli vps export |
Export hosts, redacted by default | --include-secrets, --output (-o), --i-understand-secrets-on-stdout |
ssh-cli vps import |
Import TOML or a JSON vps-export envelope |
--file, --allow-incomplete |
| Command | Purpose | Local flags |
|---|---|---|
ssh-cli connect |
Write the sibling active marker, which opens no session |
<NAME> |
ssh-cli exec |
One-shot remote command, exec-batch JSON with a selector |
<VPS> <COMMAND>, --all, --hosts, --tags, --use-active, --step, auth set, --timeout, --description |
ssh-cli sudo-exec |
One-shot sudo with safe sh -c packing |
<VPS> <COMMAND>, --all, --hosts, --tags, --use-active, --step, auth set, --sudo-password, --sudo-password-stdin, --timeout, --description |
ssh-cli su-exec |
One-shot su - elevation |
<VPS> <COMMAND>, --all, --hosts, --tags, --use-active, --step, auth set, --su-password, --su-password-stdin, --timeout, --description |
ssh-cli health-check |
Connectivity probe, health-check-batch JSON for a fleet |
<VPS_NAME>, --all, --hosts, --use-active, auth set, --timeout |
ssh-cli tunnel |
Bounded port forward with a mandatory deadline | <VPS_NAME> <LOCAL_PORT> [REMOTE_HOST] [REMOTE_PORT], --timeout-ms, --bind, --i-accept-network-exposure, --socks5, --remote-socket, --reverse, auth set |
| Command | Purpose | Local flags |
|---|---|---|
ssh-cli scp upload |
Upload a regular file, scp-transfer JSON |
--src, --dest, <VPS>, --all, --hosts, auth set, --timeout |
ssh-cli scp download |
Download a regular file, scp-transfer JSON |
--src, --dest, <VPS>, --all, --hosts, auth set, --timeout |
ssh-cli sftp upload |
Upload a file or tree over SFTP | --src, --dest, <VPS>, --all, --hosts, --recursive, auth set, --timeout |
ssh-cli sftp download |
Download a file or tree over SFTP | --src, --dest, <VPS>, --all, --hosts, --recursive, auth set, --timeout |
ssh-cli sftp ls |
List a remote directory, sftp-list JSON |
<VPS_NAME> <REMOTE>, auth set, --timeout |
ssh-cli sftp mkdir |
Create a remote directory | <VPS_NAME> <REMOTE>, auth set, --timeout |
ssh-cli sftp rmdir |
Remove an empty remote directory, previews under --dry-run |
<VPS_NAME> <REMOTE>, auth set, --timeout |
ssh-cli sftp rm |
Remove a remote file, previews under --dry-run |
<VPS_NAME> <REMOTE>, auth set, --timeout |
ssh-cli sftp stat |
Show metadata of a remote path | <VPS_NAME> <REMOTE>, auth set, --timeout |
ssh-cli sftp rename |
Rename a remote path | <VPS_NAME> <FROM> <TO>, auth set, --timeout |
| Command | Purpose | Local flags |
|---|---|---|
ssh-cli secrets status |
Show encryption status, never the key | none |
ssh-cli secrets init |
Generate and store the primary key, previews under --dry-run |
--keyring, --force |
ssh-cli secrets reencrypt |
Re-encrypt config.toml with the current key, previews under --dry-run |
none |
ssh-cli tls provider |
Show the rustls CryptoProvider status (aws_lc_rs) |
none |
ssh-cli tls paths |
Print the XDG TLS directory layout | none |
ssh-cli tls mtls list |
List imported identity names | none |
ssh-cli tls mtls import |
Import a PEM cert and key as a named identity | --name, --cert, --key |
ssh-cli tls mtls show |
Show the paths of one identity | <NAME> |
ssh-cli tls mtls remove |
Remove an identity directory | <NAME> |
ssh-cli tls acme account create |
Create an ACME account under XDG tls/acme/account.json |
--staging, --contact, --force |
ssh-cli tls acme account show |
Show whether an account exists and its path | none |
ssh-cli tls acme issue |
Start a DNS-01 order and print the TXT challenge | --domain, --staging, --print-challenge |
ssh-cli tls acme complete |
Complete the order after the DNS TXT is published | --domain |
ssh-cli tls acme status |
Show certificate or pending status | --domain |
ssh-cli tls acme list |
List ACME domain directories | none |
| Command | Purpose | Local flags |
|---|---|---|
ssh-cli completions |
Generate shell completions | <SHELL> |
ssh-cli commands |
Emit the full command tree as JSON | none |
ssh-cli schema |
Emit the schema catalog or one schema body | <NAME> |
ssh-cli doctor |
Root alias of vps doctor |
--probe-ssh, --hosts |
ssh-cli locale show |
Show the resolved language and the winning layer | none |
ssh-cli locale set |
Persist the language in the lang file (0600) |
<LOCALE> |
ssh-cli locale clear |
Remove the persisted preference | none |
- Every global flag works before or after the subcommand
| Flag | Purpose |
|---|---|
--lang <LOCALE> |
Force the UI language (en or pt-BR) |
-v, --verbose |
Log verbosity on stderr: -v info, -vv debug, -vvv trace |
-q, --quiet |
Suppress non-JSON output |
--config-dir <DIR> |
Override the configuration directory |
--no-color |
Disable colored output |
--output-format <FORMAT> |
json or text (JSON when stdout is not a TTY) |
--json |
Force JSON on stdout |
--select <PATHS> |
Keep only these dotted paths in each record, with the alias --fields |
--filter <EXPR> |
Keep records matching key=value, key!=value or key~substring |
--limit <N> |
Emit at most N records |
--sort <PATH> |
Sort records ascending by dotted path |
--dedupe-by <PATH> |
Drop later records that repeat the value of the path |
--count-only |
Replace the records with {"count": N} |
--truncate-content <CHARS> |
Shorten strings longer than N characters |
--max-output-bytes <BYTES> |
Cap the envelope size by dropping trailing records |
--no-input |
Refuse to read stdin |
--dry-run |
Print the plan of a destructive operation and exit without executing it |
--lock-wait-ms <MS> |
Wait for the config lock, default 5000, then exit 69 |
--disable-sudo |
Disable sudo-exec and su-exec for this call only, with the alias --disableSudo accepted where the global flag is accepted, and before vps add or vps edit it also writes disable_sudo: true to the host record |
--replace-host-key |
Replace a diverging TOFU host key |
--allow-plaintext-secrets |
Allow plaintext secrets at rest, for tests only |
--secrets-key-file <PATH> |
Use a 64-hex primary-key file for this call |
--use-keyring |
Prefer the OS keyring for the primary key |
--timeout <MS> |
Default timeout for SSH operations, and a local --timeout wins |
--max-concurrency <N> |
Cap multi-host sessions and tunnel forwards, from 1 to 64 |
--fail-fast |
Stop admitting new multi-host units after the first failure |
--scp-file-concurrency <N> |
Concurrent SCP transfers on one session, default 1 |
ssh-cli tunnel <vps> <port> <host> <rport> --timeout-ms Nforwards a local port, and concurrent accepts are gated by--max-concurrencyssh-cli tunnel <vps> <port> --socks5 --timeout-ms Nserves SOCKS5 (RFC 1928, no-authCONNECT), resolves names on the remote side and answersBINDandUDP ASSOCIATEwith0x07ssh-cli tunnel <vps> <port> --remote-socket PATH --timeout-ms Nforwards to a Unix socket, andPATHmust be absolutessh-cli tunnel <vps> <port> <bind> <rport> --reverse --timeout-ms Nmakes the server listen on<bind>:<rport>and deliver back to the local<port>- With
--reverse,<rport> 0lets the server allocate the port and report it inlocal_port
| Variable | Behavior | Example |
|---|---|---|
SSH_CLI_SECRETS_KEY, SSH_CLI_SECRETS_KEY_FILE |
Fail-closed: a command that resolves the primary key exits 64 when either is present | SSH_CLI_SECRETS_KEY=x ssh-cli secrets status |
SSH_CLI_HOME, SSH_CLI_LANG, SSH_CLI_FORCE_TEXT, SSH_CLI_USE_KEYRING |
Never read as config stores | ssh-cli --config-dir ./c vps list |
RUST_LOG |
Ignored by the product | ssh-cli -vv exec h id --json |
HOME |
OS home for XDG path resolution | ssh-cli --config-dir ./c vps path |
USERPROFILE |
Home fallback for ~ expansion when HOME is absent, as on Windows |
ssh-cli vps add --name w --host h --user u --key ~/.ssh/id_ed25519 |
TERM, NO_COLOR, CLICOLOR_FORCE |
Terminal and color detection only | ssh-cli --no-color vps list |
CI, FLATPAK_ID, SNAP, WSL_DISTRO_NAME, KUBERNETES_SERVICE_HOST, container, TERMUX_VERSION, TERMUX_APK_RELEASE, PREFIX |
Runtime detection only, shown as local.runtime.* in vps doctor |
ssh-cli vps doctor --json |
- Never put host passwords in environment variables
- Use the registry and stdin flags instead
| Control | How | Example |
|---|---|---|
| Config directory | --config-dir, else the XDG default |
ssh-cli --config-dir /tmp/ssh-cli-test vps list |
| Language | --lang, then ssh-cli locale set <code>, then the system, then en |
ssh-cli --lang pt-BR vps list |
| Output format | --json or --output-format json|text |
ssh-cli exec h uptime --json |
| Concurrency | --max-concurrency N (1 to 64, automatic formula when omitted) |
ssh-cli --max-concurrency 8 exec --all id --json |
| Verbosity | -v, -vv, -vvv (info, debug, trace, crate-scoped) |
ssh-cli -vv exec h 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 vps list |
- Parse one document per call:
ssh-cli exec prod "uptime" --json | jaq -r .stdout - Fan out over a label:
ssh-cli exec --tags prod "id" --json | jaq -c '.results' - Shape the payload before it exists:
ssh-cli --select name,host vps list --json - Keep secrets off argv:
printf %s "$PASS" | ssh-cli exec prod "hostname" --password-stdin --json - Rehearse a mutation, then run it:
ssh-cli --dry-run vps rename prod prod-oldand thenssh-cli vps rename prod prod-old - Branch on the exit code before parsing stdout, and read
retryablein the JSON error envelope that goes to stderr - Exit 2 from the parser and exit 64 from a malformed
--filterprint plain text on stderr even with--json, so only execution errors carry the envelope - Read INTEGRATIONS.md for agent-specific notes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
General runtime error, remote command failure (the exec JSON stays on stdout with exit_code, and stderr carries command_failed with remote_exit_code) or partial multi-host failure |
2 |
Usage error from the parser, such as two primary auth flags in one call |
64 |
Usage or invalid arguments, including invalid host names and the SSH_CLI_SECRETS_KEY and SSH_CLI_SECRETS_KEY_FILE variables |
65 |
Data error in JSON, TOML, schema or config |
66 |
VPS or input file not found |
69 |
Required service unavailable, such as a locked OS keyring or an expired config lock wait |
70 |
Internal software failure with nothing the caller can fix |
73 |
Cannot create the config or output |
74 |
IO error or SSH connection or timeout |
77 |
Authentication rejected, host key changed, sudo policy or secret decryption failure |
130 |
SIGINT |
141 |
Broken pipe on stdout or stderr |
143 |
SIGTERM |
- Prefer
--json, which is automatic when stdout is not a TTY (--output-formatoverrides) - Retry on
74and69, which the JSON error envelope marks withretryable: true - Never retry on auth (
77), usage (64,2), data (65) or not found (66) - Default tracing is
error, so exit handling and JSON stdout stay free of INFO noise ssh-cli --helpalready ends with anExamples:block and anExit status:table, and all 48 leaves showExamples:in their own--help
- The cold start goal is under 100 ms on modern Linux hosts
- Measured on one Linux host,
ssh-cli vps list --jsonon an empty registry ran in under 0.01 s with about 8 MB of peak memory - Reproduce it with
/usr/bin/time -f '%e s %M KB' ssh-cli vps list --json - Die after each command so RAM returns to the OS immediately
- Avoid long-lived tunnels, since
--timeout-msis mandatory
- Config TOML size grows with host count and path lengths
- Output buffers respect
max_output_charsper stream - The
known_hostsfile grows slowly with unique host and port pairs - Fan-out is capped by
--max-concurrency, and SCP uses one channel unless--scp-file-concurrencyis raised - No embedding model and no Node heap are required
- Rerun with
--lockedor use release 0.5.6 or newer, which pins russh 0.65.1 (seescripts/verify_install_resolve.py) - Require Rust 1.89 or newer
- Upgrade from 0.3.9 to get the SCP wire fix, which exists since 0.4.0
- Upgrade from 0.4.0 to get the honest export of empty secrets and the post-bind tunnel exit, both since 0.4.1
- Upgrade to 0.5.0 or newer for the renamed EN API, and to 0.5.3 or newer for the SFTP integrity fix
- On a key-only host, set
--keyonvps addor pass--keyor--password-stdintoexec - With a passphrase key, use
--key-passphrase-stdin - When the host key changed, confirm it is legitimate and rerun with
--replace-host-key - When
sudo-execis disabled, remove--disable-sudoand runvps edit <name> --enable-sudo
- A name is refused: use only ASCII letters, digits,
.,_and-, starting with a letter or digit - A lone positional on
execis refused: add the host, a selector or--use-active - Two auth flags in one
vps editexit 2: pass one method per call - A command is rejected as too long: raise
max_command_charsor shorten the command --dry-runon an unsupported command exits 64: use it only on the eight previewed operations
- Encrypted secrets without a key: run
ssh-cli secrets initor restoresecrets.keyor pass--secrets-key-file - The call exits 69 on the config lock: repeat it or raise
--lock-wait-ms vps doctorshowsconfig_dir_exposed: true: tighten the directory yourself withchmod 700, because the CLI never changes a directory it does not own by default- A legacy host name shows in
invalid_host_names: repair it withvps rename '<legacy name>' <new> - A config exits 65 with two hosts of the same name: rename one table in
config.toml - A TOML import exits 65: fix the parse error, and pass
--allow-incompletefor a redacted skeleton - Permission denied on the config: ensure
chmod 600onconfig.tomlandsecrets.key
- SCP moves regular files only, so use
sftp upload --recursivefor trees - A missing SCP remote exits 66 with
file not found: <path> - A failed SCP download leaves the destination absent or the previous file intact, and the partial file is
.ssh-cli.partial - A tunnel that exits 74 after
tunnel_listeningran on 0.4.0, and the post-bind deadline exits 0 since 0.4.1 - Stderr noise in JSON pipelines does not exist by default, because the level is
error - Pass
-v,-vvor-vvvto diagnose without leaking passwords, andRUST_LOG=debughas no effect - On macOS, Gatekeeper blocks the binary: run
xattr -d com.apple.quarantine /path/to/ssh-cli
- Follow the first use in docs/HOW_TO_USE.md
- Copy recipes from docs/COOKBOOK.md
- Integrate agents via docs/AGENTS.md and INTEGRATIONS.md
- Check platforms in docs/CROSS_PLATFORM.md
- Migrate from 0.3.3 or newer in docs/MIGRATION.md (target line 0.5.6)
- Run tests via docs/TESTING.md
- Consume the JSON contracts under docs/schemas/README.md
- Teach LLMs with skills/ssh-cli-en/SKILL.md
- Read CONTRIBUTING.md before opening a pull request
- Follow the bilingual documentation framework for every public doc change
- Follow the Code of Conduct
- Read SECURITY.md for private vulnerability reporting
- Prefer stdin secret flags and key files over argv passwords
- The remote stdout and stderr of
execstay raw by design - A legacy record with a control character in a tag, host or user still exits 65, and only the host NAME is tolerant
- The JSON keeps every character of the escaped set raw, such as U+202E and the zero-width space, because it is for machines
- A legacy host name raises no warning at load, and
vps doctorlists it - The global
--filterand--count-onlydo not reach thedoctorobject, and they show up inagent_shape.ignored
- Read the version history in CHANGELOG.md
russhandrussh-sftpcarry the SSH and SFTP protocolrustls,tokio-rustlsandinstant-acmecarry SSH-over-TLS, mTLS and ACMEtokioruns the async runtime andclapparses the command lineserde,serde_jsonandtomlread and write every contract and the configchacha20poly1305andkeyringprotect the secrets at rest
- Dual-licensed under MIT or Apache-2.0
- See LICENSE, LICENSE-MIT and LICENSE-APACHE