Jump Hosts
runspec jump runs a runspec tool on a remote machine over SSH+MCP. Pass the
SSH connection string directly on the command line; connection details (user,
port, key, ProxyJump, etc.) come from your ~/.ssh/config. The binary speaks
MCP JSON-RPC over stdin/stdout to a runspec serve on the remote and streams
the result back.
The model
agent / terminal remote host
│ │
│ runspec jump user@prod deploy │
│ │
│ ssh -o BatchMode=yes user@prod │
│ runspec serve │
│ ──────────────────────────────────────► │
│ │
│ ◄──── MCP JSON-RPC over stdin/stdout ──► │
│ │
│ tools/call deploy --env prod │
│ ──────────────────────────────────────► runspec serve
│ │ spawns
│ ▼
│ deploy --env prod
│ ◄──── stdout / stderr streamed back ──┤
│ exit code propagated │
The runnables are defined and installed on the remote. The local machine only
needs ssh and runspec.
Basic usage
# List tools available on the remote
runspec jump user@prod.example.com
# Run a tool
runspec jump user@prod.example.com deploy -- --env production
# Specify an exact runspec path on the remote
runspec jump user@prod.example.com --bin /opt/venv/bin/runspec deploy -- --env prod
Everything after -- is forwarded to the tool on the remote. stderr from
the remote is streamed live — not buried in a log file you have to fetch.
SSH configuration
All SSH options (user, port, private key, ProxyJump, timeouts, etc.) live in
~/.ssh/config for the given host. runspec jump passes the host string
directly to ssh with BatchMode=yes set (required because stdin/stdout are
the JSON-RPC channel).
BatchMode=yes is locked — interactive prompts would corrupt the protocol.
Use ssh-agent for keys that need a passphrase.
Example ~/.ssh/config entries:
# Simple remote
Host prod
HostName prod.example.com
User deploy
IdentityFile ~/.ssh/id_deploy
# Behind a bastion
Host internal
HostName internal.example.com
User deploy
ProxyJump bastion.example.com
ConnectTimeout 10
# CI target (non-standard port)
Host ci-target
HostName 10.0.0.5
User ci
Port 2222
IdentityFile /secrets/deploy_key
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
Then:
runspec jump prod # uses Host entry "prod"
runspec jump internal deploy -- --env staging
runspec jump ci-target run-tests
Remote runspec binary (--bin)
If runspec is not on the remote shell's PATH (common because SSH commands
run in a non-login shell and don't source ~/.bashrc), pass the full path:
runspec jump user@prod --bin /opt/venv/bin/runspec
The --bin value can also be set via environment variable:
export RUNSPEC_JUMP_BIN=/opt/venv/bin/runspec
runspec jump user@prod deploy -- --env prod
Resolution cascade (first match wins):
--binCLI flagRUNSPEC_JUMP_BINenvironment variable"runspec"(relies on remotePATH)
Basename validation: the resolved path's basename must be runspec or
runspec.exe. Anything else is rejected before SSH runs — this prevents
accidental redirection to unrelated binaries.
Trust model
runspec jump executes whatever binary lives at the resolved path on the
remote. Three forms of intent enforcement apply, but no cryptographic
protection:
- Basename locked to
runspec/runspec.exe. Accidental redirection is rejected before SSH runs. - MCP handshake required. Any process that doesn't speak JSON-RPC over
stdio fails the
initializeexchange and the call aborts. stderrstreamed live. Pre-exec output from a wrapper script appears in your terminal in real time.
These cover accidents, not adversaries. To harden further:
- Lock down remote filesystem permissions — the remote should not be writable by anyone not trusted to run code as you.
- Pin
--binto absolute paths under controlled directories (/opt/...,/usr/...) rather than user-writable locations. - Audit the runspec install on the remote the same way you'd audit any
pip install.
Privilege escalation
When a tool needs to run as a different user on the remote, use run_as
together with become_method / become_flags in the runnable's definition.
These are resolved by runspec serve on the remote before the tool runs.
Four run_as shapes
1. Simple string — same user on all hosts:
[deploy]
run_as = "oracle"
2. Environment variable — resolved at startup, useful when config management (Ansible group_vars, Salt pillars, etc.) supplies the value:
[deploy]
run_as = "$ORACLE_RUN_AS"
3. Per-host exact match — empty string means no escalation on that host:
[deploy]
run_as.default = "oracle"
run_as.hosts."special-box-01" = "dba"
run_as.hosts."special-box-02" = "" # no privilege escalation here
4. Pattern matching (regex, re.fullmatch, top-to-bottom, first match wins):
[deploy]
run_as.default = "oracle"
run_as.patterns."[lg]pexp[0-9]*" = "orasvc"
run_as.patterns."prod[0-9]*" = "produser"
Forms 3 and 4 can be combined. Resolution order: exact hosts match →
first matching patterns entry → default → no escalation.
Invalid regex patterns cause runspec serve (and runspec local) to exit
with a clear error at startup.
Become methods
[deploy]
run_as = "oracle"
become_method = "sudo" # default — also: su, pbrun, dzdo
become_flags = "-H" # passed through to the become method
| Method | Command constructed |
|---|---|
sudo |
sudo {flags} -u {user} {command} {args} |
su |
su {flags} -c "{command} {args}" {user} |
pbrun |
pbrun {flags} -u {user} {command} {args} |
dzdo |
dzdo {flags} -u {user} {command} {args} |
su uses a distinct -c "..." wrapping because it doesn't accept -u.
When the runnable declares both env args and run_as, the command is
prefixed with env KEY=val ... after the become so variables apply in the
target user's process context. This avoids requiring sudo -E / env_keep
in sudoers and needs no sshd_config changes.
Cross-platform notes
runspec jump invokes the system ssh binary. This works identically on:
- Linux / macOS — OpenSSH is the system default.
- Windows 10 (1809+) / Windows 11 — built-in OpenSSH Client at
C:\Windows\System32\OpenSSH\ssh.exe, onPATHby default. The ssh-config lives atC:\Users\<you>\.ssh\config.
If Get-Command ssh doesn't find anything on a Windows machine, the
OpenSSH Client capability is disabled. Enable it (admin):
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
PuTTY, plink, and MobaXterm can coexist on the same machine but are not used by runspec — the protocol-level requirement is OpenSSH semantics.