Ssh
Run commands, copy files, and open port forwards across a declared set of
hosts over SSH. Define the hosts once on the model, then target any subset
by host name or tag (controlplane), an explicit name list, a CEL
expression ('cel:"prod" in host.tags'), or all. Every host runs in
parallel and records its own result resource, so individual outcomes are
auditable and workflows can gate on them.
OpenSSH targets reuse long-lived connections via ControlMaster; Tailscale SSH targets authenticate with tailnet identity and bypass multiplexing. Hosts can mix both transports in one fleet, and each host can override the fleet defaults (user, port, identity, proxy jump, auth mode, and more).
Highlights
- Define your fleet once via
globalArguments.hosts[]— each host can override the fleet-default transport (kind, user, port, identity, etc.). - Mix OpenSSH and Tailscale hosts in the same fleet.
- Per-host operations land as one resource each —
run-<method>-<name>— taggedfleet/host/method/exitCode, so you can diff, gate workflows, query the audit trail by tag, and read a run's outcome straight off the returned data handle. execandscripttake an optionalokExitCodesfor guard commands whose non-zero exit is an answer, not a failure — an array replacing the default[0], or"any".- Selector forms:
"all", an explicit name list["web-1"], a bare host name or tag (controlplane),name:/tag:/cel:prefixes, or acel:predicate (e.g.'cel:"prod" in host.tags'). A bare CEL string is deprecated — prefix it withcel:. - CEL helpers bundled on every call:
matchesRegex(s, pat),cidrContains(cidr, addr). resolveanswers "which hosts does this selector match?" as pure data — no SSH connection, zero matches is a success withcount: 0, and the write'scounttag lets arunModelcaller gate without reading the resource.- Auth modes: key-based (file, agent,
IdentityAgent, inline content viaidentityContent) and password-based viasshpass -ewith the password supplied through${{ vault.get('<vault>', '<key>') }}.
Methods
| Method | What it does |
|---|---|
apply |
Materialise the host list as host-* resources; deletes stale entries that disappeared from hosts[]. |
resolve |
Resolve a host selector to the matching fleet members without connecting — writes a selection resource (addressing + tags/attrs, never credentials). Zero matches is data (count: 0), not an error. |
open |
Establish ControlMaster sockets for selected ssh hosts (no-op for tailscale). |
check |
Probe ControlMaster (ssh -O check) or run true over tailscale ssh. |
close |
Tear down ControlMaster sockets (ssh -O exit). |
exec |
Run a single shell command on each selected host; optional sudo: true prefixes sudo -n --; optional okExitCodes widens which exits count as success. |
script |
Pipe a multi-line script over stdin to sh -s -- (or chosen interpreter) — no remote-side shell-quoting from us. Takes okExitCodes too. |
copy |
scp (or rsync -e ssh) files to/from each selected host. |
forward |
Local/remote port forwarding via the master, or detached tailscale ssh -N -L child for tailscale hosts. |
collect-host-public-key |
Read and validate a remote host's SSH public key file; emit algorithm, fingerprint, and raw key as a hostPublicKey resource. |
Exit codes
A host fails when it exits with a code that isn't allowed, is killed by a
signal, or fails to spawn — and one failed host fails the method. By
default the only allowed code is 0.
exec and script take an optional okExitCodes to widen that:
| Value | Meaning |
|---|---|
| omitted | Only exit 0 succeeds. The default. |
[0, 1] |
Exit 0 and 1 succeed; everything else fails. |
[1] |
Exit 1 succeeds and exit 0 fails — the array replaces the default, it does not extend it. |
"any" |
Every exit code succeeds. |
Codes must be integers in 0..255. Spawn errors, timeouts, and signal
kills still fail the host whatever the setting — they produce no exit
status for a guard command to interpret. An allowed exit code is not a
failure and does not trip failFast. Prefer an explicit list to "any":
OpenSSH reports its own errors (unreachable host, rejected key) as exit
255, and "any" accepts those too.
# `test -f` answers 0 or 1; both are legitimate answers, not failures.
swamp model method run awesome exec \
--input hosts=all \
--input command='test -f /etc/nginx/nginx.conf' \
--input okExitCodes:json='[0, 1]' --jsonResults and tags
Each selected host records a run-<method>-<host> resource with the
verbatim exitCode, and the method returns one data handle per host in
fleet order. An allowed non-zero exit leaves the resource's error field
unset. Resources are written before any error is raised, so they are there
whether the method succeeded or failed.
Every run-<method>-<host> write is tagged:
| Tag | Value |
|---|---|
fleet |
globalArguments.name. |
host |
Host name from the fleet definition. |
method |
exec, script, or copy. |
exitCode |
Exit code as a string ("0", "1"). Absent when the process was killed by a signal or failed to spawn. |
A model calling this one via context.runModel() gets those handles back
as result.resources on success, and can branch on
handle.tags.exitCode === "0" without fetching resource content — which is
the point of okExitCodes, since a thrown method returns
{ ok: false, error } with no handles at all. In workflow CEL,
data.findByTag("host", "web-1") selects by the same tags.
See the bundled README for worked examples and per-command recipes.
Output capture
Captured by default, opt out at the fleet (captureOutput: false) or
per call. No cap — stdout/stderr land verbatim in the matching
run-<method>-<host> resource.
Requirements
- OpenSSH client (
ssh,scp) on the runner. rsyncif anycopycall setsuseRsync: true.tailscaleif any host uses thetailscaletransport.sshpassif any host usesauth.kind: password.- The swamp host must expose
ctx.createCelEnvironment()on the extension model context (introduced 2026-05).
Usage
swamp extension pull @swamp/ssh
# Create the fleet, then author its globalArguments (transport + hosts).
swamp model create @swamp/ssh awesome
swamp model edit awesome
# Run methods — every selector-taking method requires a `hosts` argument.
swamp model method run awesome open --input hosts=all --json
swamp model method run awesome exec \
--input hosts='tag:prod' \
--input command='uptime' --jsonSee the bundled README for the full configuration shape, transport options, authentication modes, and worked examples.
Resources
Modified 1 models
Added 1, removed 1 models
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned