SSH Plugin

The ssh plugin lets Vultrino hold an SSH password and drive remote deployments and commands against a host — without the calling agent ever seeing the password. It ships as a built-in plugin, so no separate installation step is needed.

Two actions are exposed:

ActionWhat it does
deployrsync a local directory to the remote host over SSH
runExecute a sequence of shell commands over SSH

Both read their inputs from credential metadata by default, so a credential alias can hold the full recipe for a specific server (source path, destination, excludes, command list). Agents then invoke the action against the alias and supply no parameters — which is also how the override-lock security model works.

System requirements

sshpass, ssh, and rsync must be on PATH.

| macOS (Homebrew) | brew install hudochenkov/sshpass/sshpass (and rsync is preinstalled) | | Debian / Ubuntu | apt install sshpass rsync openssh-client | | RHEL / Fedora | dnf install sshpass rsync openssh-clients |

The plugin will return a clear "binary not found" error if any of these are missing; it won't silently fall back.

Credential type: ssh_password

Holds the connection info and the password that will be supplied to sshpass at invocation time.

Fields

FieldTypeRequiredDescription
hosttextYesHostname or IP of the SSH server
porttextNoSSH port (default 22)
usertextYesSSH username
passwordpasswordYesSSH password (stored encrypted)

Adding via CLI

vultrino add \
  --alias prod-api \
  --type ssh_password \
  --ssh-host deploy.example.com \
  --ssh-user deploy
# prompts for SSH password

Add as many credentials as you have targets — each alias is an independent "instance" with its own host, user, password, and metadata defaults.

Configuring per-credential defaults (metadata)

Metadata is free-form key-value on the credential. The plugin looks up specific keys to populate action inputs. Set them via the vultrino meta subcommand:

vultrino meta set prod-api deploy.source_dir /path/to/local/dir/
vultrino meta set prod-api deploy.dest_dir   /opt/app/
vultrino meta set prod-api deploy.excludes   '[".git",".env","node_modules","dist"]'

vultrino meta list prod-api
vultrino meta unset prod-api deploy.excludes

Deploy keys

KeyDefaultDescription
deploy.source_dirLocal directory. Trailing / is significant to rsync — see man rsync.
deploy.dest_dirRemote directory.
deploy.excludes[]JSON array of rsync --exclude patterns.
deploy.flags-avzRsync flags as a single string.
deploy.timeout_secs1800 (30min)Kill local rsync if it exceeds this.
deploy.allow_overridefalseIf true, callers can override source_dir / dest_dir / excludes / flags in params.

Run keys

KeyDefaultDescription
run.commandsJSON array of commands. Each runs in its own SSH invocation.
run.stop_on_errorfalseIf true, halt the sequence on the first non-zero exit.
run.interval_ms0Milliseconds to sleep between commands.
run.timeout_secs300 (5min)Per-command timeout. Local ssh is killed on expiry.
run.allow_overridefalseIf true, callers can pass a custom commands array in params.

Shared keys

KeyDefaultDescription
ssh.strict_host_key_checkingaccept-newForwarded to ssh -o StrictHostKeyChecking=…. Sensible choices are accept-new, yes, no.

Actions

ssh.deploy — rsync a directory

Invoked with zero params, uses metadata defaults:

vultrino action prod-api ssh.deploy

Params (all optional):

ParamLocked by override?Description
dry_runAlways allowedRun rsync --dry-run. Useful for agents to preview.
source_dirLockedRequires deploy.allow_override=true.
dest_dirLockedRequires deploy.allow_override=true.
excludesLockedRequires deploy.allow_override=true.
flagsLockedRequires deploy.allow_override=true.
timeout_secsAlways allowedOverride the configured timeout.

Response body:

{
  "ok": true,
  "exit_code": 0,
  "stdout": "sending incremental file list\n...",
  "stderr": "",
  "duration_ms": 4213,
  "timed_out": false,
  "dry_run": false,
  "command_display": "sshpass -e rsync -avz --exclude=.git -e \"ssh -p 22 -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30\" /src/ user@host:/dest/"
}

ssh.run — execute a command sequence

vultrino action prod-api ssh.run

Params (all optional):

ParamLocked by override?Description
commandsLockedJSON array. Requires run.allow_override=true.
stop_on_errorAlways allowedHalt on first non-zero exit.
interval_msAlways allowedSleep between commands.
timeout_secsAlways allowedPer-command timeout.

Response body:

{
  "ok": true,
  "results": [
    {
      "index": 0,
      "command": "uptime",
      "ok": true,
      "exit_code": 0,
      "stdout": " 13:42:17 up 18 days, ...\n",
      "stderr": "",
      "duration_ms": 742,
      "timed_out": false
    }
  ]
}

ok at the top level is true only if every command returned 0 and none timed out.

Security model

  • Password never leaves Vultrino. Agents present a credential alias; the plugin resolves it, decrypts the password, and passes it to sshpass via the SSHPASS environment variable — never visible in ps output, never on disk.
  • Override-locked by default. An agent cannot pass a custom command list or target directory unless the credential's metadata explicitly opts in with run.allow_override=true / deploy.allow_override=true. This is intentional: a credential is a fixed recipe, and a prompt- injected agent can't turn it into "rm -rf /" without the credential owner's opt-in.
  • Host key verification. StrictHostKeyChecking=accept-new is the default — trust on first use, reject on key change. Change it via the ssh.strict_host_key_checking metadata key if you need stricter or looser behavior.
  • Timeouts actually kill. Commands that exceed their timeout have the local ssh/rsync process sent SIGKILL (tokio::Command::kill_on_drop(true)). The remote side is best-effort — SIGHUP from the closed SSH channel should reach the remote command, but commands that explicitly detach from stdio (nohup, disown, backgrounded processes) can outlive the channel. Response carries a timed_out: bool so callers don't mistake a timeout for a clean exit.

MCP exposure

Both actions are exposed as MCP tools automatically:

MCP tool nameAction
ssh_deployssh.deploy
ssh_runssh.run

Schemas include all params above plus credential and api_key.

Common gotchas

bash: command not found when you know the binary is installed

ssh host "mycommand" gives you a non-interactive, non-login shell. It doesn't source ~/.bashrc, ~/.zshrc, or (usually) ~/.profile, so PATH additions installers wrote there aren't present. Workarounds in rough order of cleanest-first:

  • Reference the absolute path: /root/.nvm/versions/node/v20/bin/node server.js.
  • Set PATH inline: PATH=$HOME/.cargo/bin:$PATH my-tool .
  • Shift the command into an existing session that has the right environment: tmux send-keys -t mysession 'my-tool' Enter — this is fire-and-forget (you won't see the command's exit code), so pair with a health probe at the end if correctness matters.

pkill -f kills itself

pkill -f pattern matches against every process's full argv. The pkill you just ran also contains pattern in its argv, so it'll match — and the shell running it gets killed, causing your SSH connection to drop with exit status 255.

Use pkill <name> without -f when matching by process name. If you really need -f, pick a pattern that can't match pkill itself (e.g. pkill -f '^/usr/local/bin/myapp').

tmux send-keys doesn't report command results

tmux send-keys returns 0 as soon as the keystrokes are delivered — not when the command inside tmux finishes. If you need to know the deployment actually started, append an explicit probe after the send-keys command, running over ssh proper:

[
  "tmux send-keys -t app 'bun install && bun run start' Enter",
  "sleep 8 && curl -fsS http://127.0.0.1:3000/health"
]

The final command's exit code gives you real status.

Example: typical deploy + restart flow

For a service running under tmux on a VPS, with the backend process in a tmux pane named app:

vultrino add \
  --alias prod-api \
  --type ssh_password \
  --ssh-host deploy.example.com \
  --ssh-user deploy

vultrino meta set prod-api deploy.source_dir /path/to/local/backend/
vultrino meta set prod-api deploy.dest_dir   /srv/app/
vultrino meta set prod-api deploy.excludes \
  '[".git",".env","node_modules","dist","tests"]'

vultrino meta set prod-api run.commands '[
  "tmux send-keys -t app C-c",
  "tmux send-keys -t app C-c",
  "sleep 1",
  "pkill -9 mybinary || true",
  "sleep 1",
  "tmux send-keys -t app '\''cd /srv/app && myinstall && myrun'\'' Enter"
]'

# Sanity-check once before relying on it
vultrino action prod-api ssh.deploy -p '{"dry_run": true}'

# Real deploy + restart
vultrino action prod-api ssh.deploy
vultrino action prod-api ssh.run

Multiple targets? Just create more aliases with their own metadata — staging-api, dr-api, etc. Same plugin, different credentials.