serverctl command reference#

All commands accept --json for machine-readable output unless noted. Errors exit with code 2 and, under --json, print {"ok": false, "error": "<Type>", "message": "..."} on stdout.

Global#

serverctl --version
serverctl doctor [--json]

doctor checks: serverctl version, platform (and whether connection reuse is available), database path, ssh / sftp / ssh-keygen presence and client version, original and managed config paths, vault backend safety, and the Python dependencies needed by the UI. Exit code is 0 only if every check passes.

For agent-driven installation, that exit code means the local installation is healthy; it is not the end of first-run onboarding and does not prove any remote host login works. After a successful doctor, an installing agent should read serverctl server list --json, summarize the real inventory, and give the user a few natural-language examples using those aliases. Do not add remote tests only for onboarding or turn the handoff into a CLI tutorial.

Web UI#

serverctl web [--port PORT] [--no-open --url-file PATH]
serverctl web --credential ALIAS [--port PORT] [--no-open --url-file PATH]
serverctl web --status [--json]
serverctl web --stop [--json]
serverctl ui [--port PORT] [--no-open --url-file PATH]
serverctl ui --credential ALIAS [--port PORT] [--no-open --url-file PATH]
serverctl ui --status [--json]
serverctl ui --stop [--json]

web names the browser shell explicitly. ui remains a compatibility alias; both use the same bundled React frontend, options, and lifecycle record. Launching a second managed runtime fails with guidance to check its status or stop it, even when two launches start at the same moment.

  • Default --port 0 picks a free loopback port and opens your browser with a one-time tokenized URL. The token is never printed to the terminal.
  • --credential ALIAS opens the browser directly on that server's edit dialog with the new-credential form expanded — the shortest path to entering a password after server add.
  • --no-open requires --url-file; the URL is written to a mode-600 file that is deleted once the token is used or the server stops.
  • If an explicit port is busy the command fails fast; retry with --port 0.
  • --status reports the UI process started by serverctl web or serverctl ui (pid, port, start time). Exit code 0 while it runs, 1 otherwise; stale records are cleaned up automatically.
  • --stop terminates that process and deletes its --url-file if one was written. It is idempotent: exit code 0 whenever no UI is left running.

Servers#

serverctl server list [--json]
serverctl server show ALIAS [--json]
serverctl server add ALIAS --hostname HOST --username USER [--port PORT]
    [--credential LABEL|ID | --password-credential LABEL] [--proxy-jump ALIAS ...] [--tag TAG ...] [--notes TEXT]
serverctl server edit ALIAS [--new-alias A] [--hostname H] [--username U]
    [--port P] [--credential LABEL|'none'] [--proxy-jump ALIAS ...]
    [--tag TAG ... | --clear-tags] [--notes TEXT]
serverctl server remove ALIAS [--yes]
serverctl server import [--config PATH] [--apply] [--overwrite] [--json]
serverctl server test ALIAS [--timeout SECONDS] [--json]
serverctl server diagnose ALIAS [--timeout SECONDS] [--json]
serverctl server note ALIAS (--text TEXT [--append] | --clear) [--json]

Notes:

  • Aliases are case-insensitively unique; whitespace, shell metacharacters, and OpenSSH wildcards are rejected.
  • list and show read saved metadata without DNS, SSH, or vault access. JSON includes nullable facts, last_success_at, last_resolved_ip, host_key, and manual geo, plus address_scope (public, private, lan, loopback, or unresolved) derived from the literal hostname.
  • --proxy-jump may repeat to build an ordered hop chain; self-jumps and cycles are rejected. Passing --proxy-jump during edit replaces the whole chain.
  • --tag may repeat to assign project, environment, or role tags. On edit, supplied tags replace the existing set; --clear-tags removes all.
  • add --password-credential LABEL creates a password credential with the hidden local prompt and attaches it in one step. It requires a real terminal; without a TTY it fails fast and points to serverctl ui --credential ALIAS.
  • import without --apply is always a preview. Conflicting aliases are skipped unless --overwrite. Overwrite preserves saved credential bindings, notes, tags, directories and Skill bindings. Preview and apply return authentication warnings with the selected source config and IdentityFile paths; import does not create managed credentials, read private-key contents or verify login. The source config remains an OpenSSH fallback, including when selected with --config. Test explicitly before treating an imported host as usable.
  • test runs a real ssh handshake (NumberOfPasswordPrompts=1, StrictHostKeyChecking) and reports latency_ms plus one of: authentication-failed, host-key-untrusted, timeout, connection-refused, dns-failed, network-unreachable, ssh-not-found, ssh-failed. Exit code 1 on failure. Failed checks replace the latest test result while retaining last_success_at and the last successful diagnostic facts. Explicit tests also save a locally resolved IP when available and host-key fingerprint/state observations (verified, unknown, changed, or revoked). The local DNS result is not a guaranteed peer IP through ProxyJump, and saved fingerprints do not replace OpenSSH host-key verification.
  • diagnose runs read-only checks for the local profile, SSH handshake, SFTP home-directory access, and a fixed remote system-summary command. The result identifies the operating system across Linux distributions (os-release with legacy-release fallbacks), macOS (sw_vers), BSDs, and Windows OpenSSH hosts, reporting structured os, os_id, os_version, os_family, and package_manager fields (e.g. debian/apt, rhel/dnf, suse/zypper, arch/pacman, alpine/apk, macos/brew) alongside kernel, CPU, uptime/load, memory, and root-disk details when the host exposes them. It never returns credentials or raw remote output; --json includes structured check results. A successful remote summary is retained as sanitized facts with observed_at, os_like, and a closed os_logo key for offline reads. Exit code 1 only when at least one check fails.
  • Changing a host's hostname or port clears saved observations and test history for the old endpoint. User notes, tags, and manual geography remain. Partial metadata updates and ProxyJump validation run within one SQLite write transaction so concurrent edits retain independent changes.
  • note lets a user or agent set, append, or clear the server's human-readable note without changing its SSH connection settings. Notes are local metadata; never put passwords, key passphrases, or other secrets in them.
  • remove prompts unless --yes; in non-interactive contexts (no TTY) it fails closed instead of assuming consent.

Credentials#

serverctl credential list [--json]
serverctl credential add-password LABEL
serverctl credential add-key LABEL --key-path PATH [--store-passphrase]
serverctl credential add-agent LABEL
serverctl credential edit LABEL|ID [--label NEW] [--key-path PATH]
    [--replace-secret] [--replace-passphrase] [--clear-passphrase]
serverctl credential remove LABEL|ID [--yes]

Notes:

  • Secrets are always collected through a hidden local prompt (getpass), never through arguments or environment variables.
  • There is no command that prints a stored secret. Reveal requires the web UI plus passkey / master-password re-authentication.
  • Private keys are locally parsed before saving to verify format and passphrase and cache public metadata. Only the key path and metadata are stored; private-key contents are never displayed or copied to the database. Removing the credential never deletes the file. Unsupported formats fail explicitly.
  • Blank replacement input preserves a saved password/passphrase. Whitespace is significant; --clear-passphrase explicitly clears a saved passphrase.
  • CLI removal still refuses credentials referenced by any server. The Web UI offers a separate reference preview and explicit Detach and delete action. Its durable journal reports committed deletion with pending vault/config cleanup accurately and supports retries across runtime restarts.
  • Independent credential testing, key Browse/Inspect/Validate and public agent identities are Web/API features, without new CLI subcommands. A Web credential test temporarily chooses that credential for any saved host, runs no remote command, and changes neither the host binding nor its health record. Agent tests use only agent keys without default-key fallback; ordinary connections retain managed OpenSSH behavior. See the Web UI.

Host-bound skills#

serverctl skill discover [--json]
serverctl skill list [--server ALIAS] [--json]
serverctl skill show NAME|ID [--json]
serverctl skill add PATH [--server ALIAS ...] [--json]
serverctl skill refresh NAME|ID [--path PATH] [--json]
serverctl skill attach NAME|ID SERVER [SERVER ...] [--json]
serverctl skill detach NAME|ID SERVER [SERVER ...] [--json]
serverctl skill remove NAME|ID [--yes] [--json]
serverctl skill resolve ALIAS [ALIAS ...] [--json]

Host-bound skills let environment-specific operating guidance follow the managed hosts where it is relevant. The relationship is many-to-many: one skill can cover a group of related nodes, and one host can use several skills. Nothing is hard-coded for a particular provider, cluster, or alias.

  • discover scans ~/.agents/skills, ~/.codex/skills, ~/.claude/skills, and directories in SSM_SKILLS_DIRS. It makes no network requests and does not install, register, or bind anything. The base ssh-server-manager transport skill is deliberately omitted and cannot be registered as host-bound guidance.
  • list shows registered skills; --server filters by a bound host. show accepts a case-insensitive name or skill ID and includes its host bindings.
  • add accepts a skill directory or its SKILL.md. It stores the normalized absolute SKILL.md path and frontmatter metadata, not the skill body. Repeated --server options register and bind atomically.
  • refresh re-reads one registered skill. --path moves the registration to another local skill directory or SKILL.md after validation.
  • attach and detach accept one or more positional server aliases and change all requested bindings atomically.
  • remove prompts unless --yes and is blocked while any host is still bound; detach those hosts first. It removes only the registry entry, never the local skill files. In a non-interactive context it fails closed without --yes.
  • Names are case-insensitively unique. Discovery reports same-name candidates at different paths as conflicts, and registration refuses to guess between them.

discover --json returns {"candidates": [...], "conflicts": [...]}. Every candidate has path and status; a valid candidate also has name and description. Candidate status is:

  • available — valid local skill that is not registered;
  • registered — its name and path match a registry entry, whose ID is in registered_id;
  • conflict — its name or path disagrees with another candidate or registry entry;
  • invalid — its frontmatter could not be validated; the reason is in error.

Each conflict has a type. A name conflict reports name and paths; a path conflict reports path and names. Conflicts against an existing registry entry also include registered_id. Discovery status describes candidates, not whether a registered skill is currently usable.

Agents call resolve only after selecting the target alias or aliases. Its JSON has both a per-host view and a deduplicated view:

{
  "ok": true,
  "hosts": [
    {"id": "...", "alias": "gpu-lab-01", "skills": [{"id": "...", "name": "gpu-ops", "path": "/.../gpu-ops/SKILL.md", "description": "...", "status": "ready"}]}
  ],
  "skills": [
    {"id": "...", "name": "gpu-ops", "path": "/.../gpu-ops/SKILL.md", "description": "...", "status": "ready", "applies_to": ["gpu-lab-01"]}
  ]
}

The response contains metadata and local paths, never skill bodies. Each skill's applies_to list is authoritative for a multi-host task. Current file status is ready, missing, invalid, or name_mismatch; any non-ready registration makes ok false and exits 1 instead of silently substituting a different path. skill list and both views in skill resolve include a status_message for non-ready entries; ready entries omit it. skill show returns stored registry metadata and host bindings without calculating readiness.

Saved working directories#

serverctl path list ALIAS [--json]
serverctl path add ALIAS PATH --label LABEL [--note TEXT] [--json]
serverctl path edit ALIAS LABEL|ID [--label NEW] [--path NEW] [--note TEXT] [--json]
serverctl path remove ALIAS LABEL|ID [--yes] [--json]
serverctl path resolve ALIAS [ALIAS ...] [--json]

A saved working directory records where you work on a host, so the browser and the agent both stop guessing. Unlike skills, the relationship is one-to-many: a path is a claim about one machine's filesystem, and removing the host removes its saved directories.

  • Labels are unique per host, case-insensitively, and resolve the same way skill names do — serverctl path edit web1 APP finds the entry labelled app. Paths are unique per host too, so the same directory cannot be saved twice under different names.
  • add refuses an empty path. Everywhere else an empty remote path means the home directory; a saved directory has to name something, so write ~ explicitly if that is what you mean.
  • resolve is the command an agent should reach for before touching files. It answers "where does this fleet keep its application" in one read, with no connection: identical paths across hosts collapse into a single entry whose applies_to lists every host that has it.
  • Ordering is by most recent use, then alphabetical. Directories that have never been opened stay in alphabetical order rather than jumping to the top.

Example resolve output:

{
  "ok": true,
  "hosts": [
    {"id": "...", "alias": "web1", "paths": [{"label": "app", "path": "/srv/app", "notes": "", "last_used_at": null}]},
    {"id": "...", "alias": "web2", "paths": [{"label": "app", "path": "/srv/app", "notes": "", "last_used_at": null}]}
  ],
  "paths": [
    {"path": "/srv/app", "label": "app", "applies_to": ["web1", "web2"]}
  ]
}

File transfer#

serverctl cp SOURCE DEST [--force] [--timeout SECONDS] [--json]
serverctl get ALIAS:REMOTE [LOCAL] [--force] [--timeout SECONDS] [--json]
serverctl put LOCAL ALIAS:REMOTE [--force] [--timeout SECONDS] [--json]

Transfers go through the same OpenSSH SFTP path as everything else, so vault passwords and key passphrases are injected for you. This is the reason to prefer them over scp and rsync, which prompt because they never see the vault.

  • Exactly one side names a host as ALIAS:PATH. Host-to-host copies are refused rather than silently staged through this machine.
  • ALIAS:PATH splits on the first colon, so remote filenames may contain colons. A Windows drive letter is never read as an alias: C:\Users\me is local even though C is a legal alias.
  • A destination that is an existing directory receives the remote filename, the way cp behaves. An existing local file is never overwritten without --force.
  • Without --json the transfer streams sftp's own progress meter. With --json it is captured and reported as bytes and duration.
  • One file at a time. Recursive transfers are not supported; use serverctl mount or a remote archive through serverctl exec for a whole tree.

Browser transfers use a separate 100 MiB limit, advertised by /api/v1/runtime. HTTP uploads refuse existing destinations by default; explicit overwrite=true permits an authorized replacement. The CLI command surface is unchanged: CLI transfers have no web size cap, and CLI put can replace an existing remote file. Default web uploads require SFTP hard-link support and fail safely when unavailable; web downloads refuse an unverifiable symlink target size. See the web UI for the versioned file API.

Mounting#

serverctl mount ALIAS[:REMOTE] [MOUNTPOINT] [--read-only] [--json]
serverctl mount --list [--json]
serverctl unmount ALIAS|MOUNTPOINT [--json]

Mounting exposes a remote directory to Finder, Explorer, and your editor through sshfs, reusing the managed config and the same credential boundary as every other connection.

  • This is the only feature that needs software we cannot install for you: macFUSE on macOS, libfuse and fusermount on Linux, SSHFS-Win plus WinFsp on Windows. Run serverctl doctor first — the mount row names the missing piece rather than failing at mount time.
  • The default mount point is ~/mnt/ALIAS. It is created if missing and refused if it already has contents.
  • --list reads the operating system's mount table rather than a stored list, so unmounting in Finder or with umount can never leave it out of date.
  • --read-only is worth using whenever you only need to read; it removes the possibility of an editor writing to production by accident.

Connect and execute#

serverctl connect ALIAS [--timeout SECONDS]
serverctl exec ALIAS [--shell] [--stdin | --stdin-binary]
    [--timeout SECONDS] [--reuse SECONDS] [--json] -- COMMAND [ARG ...]
  • connect opens an interactive shell and requires a real terminal. Without a TTY (pipes, agent tool calls, cron) it fails fast with exit code 2 and points to exec instead of hanging.
  • With OpenSSH's normal ask policy, the first connection shows the server's host-key fingerprint in that terminal, including when a saved password or passphrase is supplied by AskPass. Check it against a trusted source before confirming with yes or the matching fingerprint. A missing terminal or declined confirmation fails closed. Stricter SSH policies still apply; changed or revoked keys require investigation rather than automatic removal.
  • Everything after -- is the remote command. Without --shell, each argument is quoted for a POSIX remote shell — write exec web1 -- ls -la /var/log, not a single string.
  • --shell accepts exactly one string and runs it via remote sh -lc; use it for pipelines, redirects, globs, and && chains.
  • --stdin streams UTF-8 text; --stdin-binary streams raw bytes.
  • --reuse N enables OpenSSH ControlMaster for N seconds so consecutive exec calls skip re-authentication. macOS/Linux only; on Windows a warning is printed and the option is ignored.
  • Exit code mirrors the remote command's return code. --json adds stdout, stderr (redacted), latency_ms, and error_code.

Config#

serverctl config render [--json]

Re-renders the managed OpenSSH config (also done automatically after every mutation) and prints its path. The file is written atomically with user-only permissions. Imported hosts retain their selected source config; unrelated aliases and defaults use the original config. Source paths are formatted for the selected SSH client, including native Windows and Git/MSYS OpenSSH. SFTP explicitly uses the same SSH client. Your source configs are never rewritten.

Edit this page on GitHub