Command reference#
Install the hoploom distribution and use hoploom. Existing serverctl
commands remain compatible. Never install the unrelated PyPI serverctl
package to repair PATH; locate the installed launcher first. See
../docs/migration.md for package upgrades.
Setup and UI#
./scripts/bootstrap
./scripts/hoploom doctor [--json]
./scripts/hoploom web [--port PORT] [--no-open --url-file PATH]
./scripts/hoploom web --credential ALIAS [--port PORT] [--no-open --url-file PATH]
./scripts/hoploom web --status [--json]
./scripts/hoploom web --stop [--json]
./scripts/hoploom ui [--port PORT] [--no-open --url-file PATH]
./scripts/hoploom ui --credential ALIAS [--port PORT] [--no-open --url-file PATH]
./scripts/hoploom ui --status [--json]
./scripts/hoploom ui --stop [--json]
web is the browser-shell entry point; ui is a compatibility alias with
identical options. Both open the bundled modern React UI and share one managed runtime
record. A second launch fails while a managed runtime is active; use --status
and the existing window rather than stopping someone else's active session.
In the Web UI, a deliberate host-name hover lazily observes missing/stale facts with field skeletons, single-flight requests, 60-second freshness and a 30-second failure cooldown. Page load and keyboard focus stay local. Hide details disables previews and collection, retains fixed masked columns and disables copying; Shift+H toggles it outside focused controls. Credential tests require Run test and can use an unbound credential against any saved host without changing its binding. An unconfirmed TCP/host-key checkpoint or a failed jump route is not evidence that the target credential was rejected. These are browser features; they add no CLI commands.
The default --port 0 selects an available loopback port and opens the browser.
When --no-open is used, --url-file is required; the file is created with
mode 600 and contains the one-time tokenized URL. The token is never printed.
If an explicitly requested port is occupied, retry with --port 0 or choose a
different port.
ui --credential ALIAS opens the browser directly on that server's edit
dialog with the new-credential form expanded, so entering a password is one
field and one click. Use it right after server add when guiding a user
through first-time setup.
ui --status reports the managed UI process (pid, port, start time) and exits
0 while it runs. ui --stop terminates it and deletes its --url-file; the
command is idempotent, so use it to clean up when the user is done with the UI
instead of tracking pids by hand.
Agent post-install handoff#
doctor is a local health check, not the completion condition for an
agent-driven installation. After it succeeds, an installing agent should run
hoploom server list --json once, summarize the actual managed hosts, and
teach the user what they can now ask for in natural language using those real
aliases. Do not add remote server test calls only for onboarding, do not lead
with database/config paths or raw JSON, and do not require the user to learn
CLI syntax. Make clear that stored secrets remain in the local OS vault and
that a successful doctor does not prove a host login has been tested.
Servers#
./scripts/hoploom server list [--json]
./scripts/hoploom server show ALIAS [--json]
./scripts/hoploom server add ALIAS --hostname HOST --username USER [--port PORT]
[--credential CREDENTIAL | --password-credential LABEL] [--proxy-jump ALIAS ...] [--notes TEXT]
./scripts/hoploom server edit ALIAS [field options]
./scripts/hoploom server remove ALIAS [--yes]
./scripts/hoploom server import [--config PATH] [--apply] [--overwrite] [--json]
./scripts/hoploom server test ALIAS [--timeout SECONDS] [--json]
./scripts/hoploom server diagnose ALIAS [--timeout SECONDS] [--json]
./scripts/hoploom server note ALIAS (--text TEXT [--append] | --clear) [--json]
An import without --apply is always a preview. Literal aliases are resolved with ssh -G; wildcard-only patterns are reported but not imported.
Import warnings identify authentication directives that are not converted into
managed credentials, configured IdentityFile paths and the selected source
config. Applied hosts retain that config as an OpenSSH fallback, including a
custom --config path. Import reads no private-key contents and does not verify
login. --overwrite updates imported connection fields while preserving the
latest credential binding, notes, tags, directories and Skill bindings. An
empty credential binding may still authenticate through the source config;
confirm with an explicit server test.
server add --password-credential LABEL creates a password credential and
attaches it in one step, using the same hidden local prompt as
credential add-password. It needs a real terminal; without a TTY it fails
fast and points to hoploom ui --credential ALIAS instead.
diagnose identifies the host's system: its remote check reports os,
os_id, os_version, os_family, and package_manager (Linux distros via
os-release, macOS via sw_vers, BSDs, and Windows OpenSSH hosts) plus kernel,
arch, CPU, memory, and disk. Use recent cached facts, or run it when those facts
are missing or stale, before choosing install/admin commands; it is read-only and heavier
than test, so run it on demand, not as a routine sweep. A successful remote
summary is saved as sanitized facts, including observed_at, os_like, and
a closed os_logo key, for subsequent offline inventory reads.
test only checks connectivity; it does not collect system facts. The UI's
Collect host info action explicitly runs a handshake and the same system
probe as diagnose, without SFTP. It supports one host or a selected batch
of 1-64 hosts with concurrency 1-4 and timeout 1-120 seconds per probe (plus
the SSH runner's bounded transport grace). Failed probes keep prior facts.
This is also available via POST /api/v1/hosts/{id}/observe and
POST /api/v1/hosts/observe; the name-hover preview uses the single-host endpoint
on demand, then reads the updated local Host DTO. The inventory no longer has
per-row or page-wide collection buttons; explicit collection remains in host
details and the selected-host toolbar. There is no new CLI batch command.
list --json and show --json read local metadata without DNS, SSH, or vault
access. They include nullable facts, last_success_at, last_resolved_ip,
host_key, and geo, plus the literal address_scope. A failed test updates
the latest test result but retains the last successful timestamp and facts.
The resolved IP is a local DNS snapshot from an explicit check, not proof of
the peer IP through ProxyJump. Host-key states are verified, unknown,
changed, or revoked; saved state never bypasses OpenSSH verification.
Changing hostname or port clears the old endpoint's observations.
note changes only local server metadata. Use --text to replace a note,
--append to add an agent or user observation without losing existing text, or
--clear to remove it. Never put passwords or key passphrases in notes.
Credentials#
./scripts/hoploom credential list [--json]
./scripts/hoploom credential add-password LABEL
./scripts/hoploom credential add-key LABEL --key-path PATH [--store-passphrase]
./scripts/hoploom credential add-agent LABEL
./scripts/hoploom credential edit LABEL [--label NEW_LABEL] [--key-path PATH]
[--replace-secret] [--replace-passphrase] [--clear-passphrase]
./scripts/hoploom credential remove LABEL [--yes]
./scripts/hoploom credential rotate ALIAS [--new-label LABEL] [--timeout SECONDS] [--json]
Secret input uses a local hidden prompt. There is deliberately no CLI command that reveals a saved secret.
edit --replace-secret changes only the copy saved on this machine; it does not
check or change the server. rotate is for a host whose login password is
changing: it needs a terminal, takes the new password at a hidden prompt, waits
while the human changes it on the server with the account's own tools, proves a
fresh password-only login (no agent, key, or shared connection), and only then
saves it. A rejected password saves nothing and is followed by one check of the
saved password, so the output says which one the server accepts. When both are
rejected, or a jump host rejects its own saved credential (the message names
it), the command stops rather than spend more login attempts. A credential no
other host uses is updated in place; a shared one keeps the old password for the
other hosts, and ALIAS is bound to a new credential LABEL (ALIAS) unless
--new-label names it. Each check is recorded in the audit log as
password_rotate. hoploom never runs passwd for you.
Keys are locally parsed before saving, supporting OpenSSH/PEM RSA, ECDSA and
Ed25519 identities. Unsupported formats fail explicitly. An encrypted key needs
its passphrase through the hidden prompt or local UI; never provide one in chat,
argv or environment variables. The private-key path and public metadata are
saved, not its contents. Empty replacement input preserves the existing value;
--clear-passphrase is the explicit removal action. Whitespace is significant.
remove still refuses a credential referenced by any host. The Web Credentials
view provides a separate preview and explicit Detach and delete transaction,
with durable pending vault/config cleanup and Retry cleanup when needed.
Neither operation removes a private-key file. A committed deletion with pending
cleanup must not be presented as completed secret removal.
The Web view also exposes local key Browse/Inspect/Validate, agent identities
and Test on host. There is no new credential-test CLI command. The Web test
authenticates the selected credential against any saved host without rebinding
it, executing a remote command, or updating host health history. Password/key
tests restrict authentication to the selected kind; agent tests use only agent
keys without default private-key fallback, unlike ordinary connections that
follow the managed OpenSSH configuration. A none authentication result is not
credential verification. Progress/results never return secrets. See
the local UI and API contracts.
Host-bound skills#
./scripts/hoploom skill discover [--json]
./scripts/hoploom skill list [--server ALIAS] [--json]
./scripts/hoploom skill show NAME|ID [--json]
./scripts/hoploom skill add PATH [--server ALIAS ...] [--json]
./scripts/hoploom skill refresh NAME|ID [--path PATH] [--json]
./scripts/hoploom skill attach NAME|ID SERVER [SERVER ...] [--json]
./scripts/hoploom skill detach NAME|ID SERVER [SERVER ...] [--json]
./scripts/hoploom skill remove NAME|ID [--yes] [--json]
./scripts/hoploom skill resolve ALIAS [ALIAS ...] [--json]
discover scans the standard local skill roots (~/.agents/skills,
~/.codex/skills, and ~/.claude/skills) plus SSM_SKILLS_DIRS. It is
read-only and offline: candidates are not installed, registered, or bound.
The base hoploom transport skill (including its legacy ssh-server-manager name) is omitted and cannot be
registered as host-bound guidance.
add accepts either a skill directory or its SKILL.md; each repeated
--server creates a binding in the same transaction. Skill names are
case-insensitively unique, so same-name candidates at different paths are an
explicit conflict rather than an arbitrary choice.
discover --json returns candidates and conflicts. Candidate status is
available, registered, conflict, or invalid; valid candidates include
name, description, and path, registered matches include registered_id,
and invalid candidates put the validation reason in error. Name conflicts
contain name plus paths; path conflicts contain path plus names;
registry conflicts also include registered_id.
refresh re-reads the registered SKILL.md, optionally from a replacement
path. attach and detach update bindings for all positional server aliases.
remove is blocked while any host is bound; detach those hosts first.
remove --yes skips only the confirmation and never deletes local skill
files.
Agents should call resolve after determining the target hosts and again
whenever the target changes. The JSON contains both per-host skills and a
deduplicated skill list whose applies_to aliases preserve multi-host scope.
It returns metadata and paths, not skill bodies. missing, invalid, and
name_mismatch registrations make ok false and the command exit 1; do not
silently reuse a previous host's skill context. list and resolve add
status_message to non-ready entries; show returns stored metadata and host
bindings without calculating readiness.
Saved working directories#
./scripts/hoploom path list ALIAS [--json]
./scripts/hoploom path add ALIAS PATH --label LABEL [--note TEXT] [--json]
./scripts/hoploom path edit ALIAS LABEL|ID [--label NEW] [--path NEW] [--note TEXT] [--json]
./scripts/hoploom path remove ALIAS LABEL|ID [--yes] [--json]
./scripts/hoploom path resolve ALIAS [ALIAS ...] [--json]
Call path resolve after choosing the target hosts and before browsing or
transferring anything. It returns, in one read and without connecting, the
directories the user has marked as theirs on those hosts — which is the
difference between working in /srv/app and guessing at /var/www. The
paths array collapses identical paths across hosts and lists every host that
has one in applies_to, so a fleet-wide action can be scoped in a single step.
Saved directories are one-to-many, unlike skills: each belongs to one host, and labels are unique per host case-insensitively. They are metadata the user wrote, not a directory listing — never present them as proof a path exists.
File transfer#
./scripts/hoploom cp SOURCE DEST [--force] [--timeout SECONDS] [--json]
./scripts/hoploom get ALIAS:REMOTE [LOCAL] [--force] [--timeout SECONDS] [--json]
./scripts/hoploom put LOCAL ALIAS:REMOTE [--force] [--timeout SECONDS] [--json]
Use these instead of scp, rsync, or sftp. They run the same SFTP path as
the rest of the tool, so a vault password or key passphrase is injected without
prompting; the raw tools cannot see the vault and will prompt or fail.
Exactly one side is ALIAS:PATH. One file per invocation, or one whole tree
with --recursive (put --recursive DIR ALIAS:PATH,
get --recursive ALIAS:DIR LOCAL), which creates the destination directory.
get refuses to overwrite an existing local file without --force — treat that
refusal as a question for the user, not an obstacle to route around. Recursive
copies refuse symlinks inside the tree and stop at 4000 entries; do not loop
around either refusal without telling the user.
The browser's separate HTTP transfer interface is capped at 100 MiB per file
(advertised as max_web_transfer_bytes by /api/v1/runtime). Its uploads
default to refusing an existing remote destination with HTTP 409; an
authorized overwrite uses the API's explicit overwrite=true form field.
Default web uploads require the server's SFTP hard-link operation for safe
no-replace publication; unsupported servers return an error. Web downloads
refuse symlinks when the target size cannot be verified. These HTTP options add
no CLI commands or flags: CLI transfers have no web size cap, and CLI put
retains its existing remote-replacement behavior.
Mounting#
./scripts/hoploom mount ALIAS[:REMOTE] [MOUNTPOINT] [--read-only] [--json]
./scripts/hoploom mount --list [--json]
./scripts/hoploom unmount ALIAS|MOUNTPOINT [--json]
Mounting needs a FUSE stack that may not be installed: macFUSE, libfuse, or
SSHFS-Win with WinFsp. Check the mount row of hoploom doctor before
offering it, and report the missing component instead of retrying. Prefer
--read-only unless the user asked to write.
Port forwarding#
./scripts/hoploom tunnel list [--server ALIAS] [--json]
./scripts/hoploom tunnel show NAME|ID|ALIAS:NAME [--json]
./scripts/hoploom tunnel add NAME --server ALIAS (--local SPEC | --remote SPEC | --dynamic SPEC) [--note TEXT] [--json]
./scripts/hoploom tunnel edit NAME [--name NEW] [--server ALIAS] [--note TEXT] [--spec SPEC] [--kind KIND] [--json]
./scripts/hoploom tunnel open NAME [--timeout SECONDS] [--foreground] [--json]
./scripts/hoploom tunnel close NAME [--json]
./scripts/hoploom tunnel status [--server ALIAS] [--json]
./scripts/hoploom tunnel remove NAME [--yes] [--json]
Tunnels are saved as profiles and opened explicitly: tunnel add writes
metadata and contacts nothing, tunnel open starts one detached ssh -N and
returns its pid, tunnel close stops it. Never start a forward by running
ssh -L yourself; the profile is what keeps the saved shape and the running
process identical, and the running process is what status can prove.
SPEC uses OpenSSH's own spelling: PORT:HOST:PORT or
BIND:PORT:HOST:PORT, and PORT or BIND:PORT for --dynamic. The host half
resolves on the remote side, so --local 15432:localhost:5432 reaches the
target host's own localhost:5432. The listening side defaults to 127.0.0.1;
pass a bind address only when the user asks for a forward other machines should
reach, because that is a remote-access hole rather than a convenience.
Profile names are unique across all hosts, so tunnel open db1-local is
unambiguous, and ALIAS:NAME is accepted where it helps a human read the
command. open on a running profile reports already_open instead of starting
a second process, and close on a saved profile that is not running is a no-op,
so a teardown sequence can be repeated safely.
tunnel status joins saved profiles with this machine's live processes. Treat
running: true as fact. A running row with orphan: true means the profile was
deleted while its process survived: close it with tunnel close under that
row's name before telling anyone the port is free. Runtime records live in the
private runtime directory and are pruned as soon as the process is gone, so a
copied database never claims a tunnel is up. Background tunnels belong to this
machine; after a reboot, re-open the ones the user still needs.
Export and import#
./scripts/hoploom export [--output PATH] [--json]
./scripts/hoploom import [FILE|-] [--apply] [--overwrite] [--json]
Use these instead of hand-editing the database or copying manager.db between
machines. The export is deterministic (sorted hosts, fixed key order, no
timestamps), holds no secrets, and carries a host's credential only as its
label. import previews by default and writes with --apply; existing hosts
need --overwrite, while saved directories and tunnels are merged so an omitted
entry never deletes local work. When a credential label is unknown on this
machine, the host is imported without a credential and the warning says so —
report that rather than claiming the host is fully restored.
Host locations (local GeoIP)#
./scripts/hoploom geo status [--json]
./scripts/hoploom geo resolve HOST [HOST ...] [--apply] [--overwrite] [--json]
./scripts/hoploom geo resolve --all [--apply] [--overwrite] [--json]
Reads a local range file (SSM_GEOIP_CSV) or a local .mmdb when geoip2 is
installed. There is no online lookup and no lookup on a read path, so never
describe a host's location as known until geo resolve has actually been run
and applied. Without --apply nothing is stored; a lookup never replaces a
manual geo value unless --overwrite is passed; a country here is an inference
about address routing, not a statement about where data is stored.
MCP server#
DeepSeek Harness can install the dsh-hoploom adapter with
dsh plugin --profile web add dsh-hoploom; use tui for the terminal profile.
It runs a local Hoploom executable with the policy below and defaults to
metadata queries. Configure remote access in the plugin settings, never by
working around a missing tool. See the plugin guide.
Plugin 0.2.0 and later reuse an existing installation or automatically prepares its bundled
backend and Python. The Hoploom sidebar shows progress and retry/cancel, then
embeds the full original UI without opening an external browser. In terminal sessions, hoploom_setup accepts action=status,
prepare or cancel. Report setup status once; do not busy-poll. An explicit
custom command must support MCP; old serverctl 0.7.0 does not.
./scripts/hoploom mcp serve [--allow-remote] [--allow-exec] [--allow-command PATTERN]
[--http] [--port N] [--token-file PATH]
./scripts/hoploom mcp tools [--allow-remote] [--allow-exec] [--allow-command PATTERN] [--json]
Only for clients that cannot read this Skill. Read-only unless --allow-remote
(adds test_host) and --allow-exec with at least one --allow-command glob
(adds run_command, argv-only). There is no reveal tool and no secret in any
result. mcp tools lists what a policy exposes before you wire up a client, and
every acting call is audited under actor mcp. Do not start it in a terminal a
human is using for anything else: stdout is the protocol channel.
--http switches to a loopback HTTP endpoint (POST /mcp) for clients that
cannot spawn a process. Same server, policy and audit trail; the transport alone
changes. It binds 127.0.0.1, rejects a non-loopback Host, requires
Authorization: Bearer <token> on every request, and rejects any request with an
Origin header — a browser cannot drive it even if it learns the token. The
token is printed once to stderr at launch; --token-file writes it to a mode-600
file instead, which is what a client config should read. --port 0 (the default)
picks a free port. Prefer stdio whenever the client can spawn a process.
Audit log#
./scripts/hoploom audit list [--limit N] [--alias ALIAS] [--event KIND] [--since ISO] [--json]
./scripts/hoploom audit path [--json]
./scripts/hoploom audit rotate [--force] [--json]
The log is local, append-only, and mode 600. It records exec and
exec_result, reveal and reveal_denied, connect, tunnel_open, and
tunnel_close. Read it when the user asks what has been run on a host, or
before claiming an action left no trace. It is evidence of attempts and
outcomes, not proof of state: exec_result records the exit status the tool
saw, and a log that cannot be written is reported once on stderr rather than
blocking the command.
The file rotates itself at 5 MiB into audit.log.1, keeping three segments, and
every read spans all of them — so a rotated log still answers "what happened"
without asking the user to merge files.
The log never contains a secret. A reveal is recorded by credential label and slot; credentials typed into a command line are masked before the event is written. Do not copy log lines into a summary if they still look like they carry a value; report the event and label instead.
Connections#
./scripts/hoploom connect ALIAS [--mosh] [--log | --log-path FILE]
./scripts/hoploom exec ALIAS [--stdin|--stdin-binary] [--reuse SECONDS] [--shell] [--json] -- COMMAND [ARG ...]
./scripts/hoploom config render [--json]
--log records the session to a mode-600 transcript; only the display stream
is recorded, so no-echo password prompts stay out of the file. Read it with
the user's consent — a transcript is session content, not tool metadata.
connect opens an interactive shell and is meant for a human terminal, not
for agent tool calls (see SKILL.md). --mosh uses the mosh client instead,
which needs mosh-server on the host and a local mosh (the mosh row of
hoploom doctor reports it); it bootstraps over the same managed ssh config
and keeps host-key verification, and it fails rather than falling back to ssh. Without a TTY it fails fast with exit
code 2 and a pointer to exec.
With the normal OpenSSH ask policy, an unknown host key is shown in the
human's controlling terminal even when AskPass supplies a saved password or
key passphrase. Compare its fingerprint with a trusted source before typing
yes or the matching fingerprint; no, EOF, or an unavailable terminal
rejects confirmation. Explicit stricter SSH policies remain in force. This
does not authorize agent calls, checks, transfers, or credential tests to
accept keys, and it does not replace a changed or revoked key.
exec quotes command arguments for a POSIX remote shell. --shell accepts
exactly one command string and runs it through remote sh -lc; use it for
pipelines, redirects, command substitutions, or multiple commands. Without
--shell, pass the executable and each argument separately. For a non-POSIX
remote shell, pass arguments appropriate for that server and omit --shell.
--stdin is UTF-8 text. --stdin-binary streams raw bytes without a text
decode, which is suitable for artifacts piped to a remote command. Do not put
secrets in command arguments, environment variables, or temporary files.