InitRunner

SSH Backend

The SSH backend runs tool subprocesses on an existing remote host over OpenSSH. It is remote execution, not a kernel sandbox. The host's existing isolation, whatever it is, is what your agent's tools get. Use it to choose where code runs (a build server, a GPU box, a customer staging VM), not to contain untrusted code.

If you need isolation, use Bubblewrap or Docker Sandbox instead. For the cross-backend config reference, see Runtime Sandbox.

Available since v2026.5.1.

Quick Start

apiVersion: initrunner/v1
kind: Agent
metadata:
  name: remote-build-agent
spec:
  role: You are a build assistant that runs commands on the build host.
  model:
    provider: openai
    name: gpt-5-mini
  tools:
    - type: shell
  security:
    sandbox:
      backend: ssh
      ssh:
        host: my-build-box       # alias from ~/.ssh/config, or user@hostname
        remote_cwd: /srv/work    # optional working directory on the remote host

That is the minimum. The host alias is resolved through your existing ~/.ssh/config and ssh-agent. No keys live in InitRunner config.

# Sanity-check the connection out of band
ssh -o BatchMode=yes my-build-box true && echo OK

Configuration reference

All SSH-specific fields live under security.sandbox.ssh.

FieldTypeDefaultDescription
hoststr(required)Host alias from ~/.ssh/config or user@hostname.
remote_cwdstr | nullnullWorking directory for every remote command. If unset, the SSH login directory is used.
identity_filestr | nullnullOverride IdentityFile. Prefer setting this in ~/.ssh/config instead.
config_filestr | nullnullOverride ~/.ssh/config path (rarely needed).
connect_timeoutint10Seconds for the initial connection.
control_persiststr"60s"How long the multiplexed connection stays warm between calls. Any OpenSSH duration string.

How it works

Every tool call shells out to ssh -- <host> <remote-command> with ControlMaster=auto, so the second and subsequent calls reuse a warm connection. Per-call latency on a fresh socket is roughly 150 to 500 ms; reused, it is in the tens of ms.

The remote command is constructed as:

[cd <remote_cwd> && ] [env VAR=val ...] <argv...>

argv and the env mapping that the tool passed in are shell-quoted with shlex.quote. Sensitive env keys (anything matching the same prefix and suffix list other backends use, such as *_KEY, *_TOKEN, OPENAI_API_KEY, AWS_*) are stripped from the remote env before it leaves the local machine.

Authentication

InitRunner does not handle SSH auth. The local ssh process inherits the parent environment unchanged, including SSH_AUTH_SOCK and SSH_AGENT_PID, so:

  • ssh-agent and ssh-add work as you would expect.
  • ~/.ssh/config Host blocks are honored (User, Port, IdentityFile, ProxyJump, ForwardAgent, and so on).
  • Hardware keys, FIDO/U2F, and OpenSSH certificate auth all work because they work in your shell.

If you set identity_file in YAML, it is threaded through as ssh -i <path>.

What is NOT supported in v1

These fields and concepts do not apply to a real remote filesystem and are explicitly rejected at config load:

FieldReason
bind_mountsNo shared filesystem. v1.1 will add SCP staging.
allowed_read_pathsSame.
allowed_write_pathsSame.
network: bridgeSSH cannot enforce remote network policy. Use none (informational) or host.

These fields are accepted but inert under SSH (kept so backend: ssh can be added to an existing role without touching unrelated config):

  • read_only_rootfs does nothing; the remote rootfs is whatever it is.
  • memory_limit and cpu_limit have no remote enforcement in v1.
  • docker.* is ignored.

Tools that do not work over SSH in v1

  • python_exec stages a local file and bind-mounts it as /work/_run.py. Without SCP staging, there is no way to deliver the file. The tool fails fast with a v1.1 remediation message. Workaround: install Python on the remote host and use shell with python -c "...", or check a script into the remote machine ahead of time.
  • Anything else that uses extra_mounts. The backend rejects non-empty extra_mounts at runtime with a clear SandboxConfigError.

Coming in v1.1

  • Stdin-piped python_exec (no filesystem staging).
  • SCP-based mount staging for extra_mounts.

Security posture

This is the bit that bites if you skim. SSH does not:

  • isolate the agent's commands from the rest of the remote host's filesystem,
  • enforce memory or CPU limits,
  • prevent network access,
  • contain a malicious or buggy tool.

Use it for trusted-but-remote execution. If your role's tools could be coaxed into running attacker-controlled commands, run those tools through bwrap or docker on a host you do not mind compromising, not via SSH on production infrastructure.

Audit

Every remote call logs a sandbox.exec event with backend=ssh host=<host> argv0=<first-token> rc=<n> duration_ms=<ms>. Identity files, full argv arguments, and command output are not logged.

Query with:

initrunner audit security-events --event-type sandbox.exec

Troubleshooting

ssh client not found on PATH — install OpenSSH:

apt install openssh-client     # Debian/Ubuntu
brew install openssh           # macOS
dnf install openssh-clients    # Fedora

ssh probe to '<host>' returned rc=255 — usually auth or hostname. Reproduce out of band:

ssh -o BatchMode=yes -v <host> true

ssh-agent not runningssh-add -l should list a key. If it says "Could not open a connection to your authentication agent," start one and add your key:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

ControlMaster socket lingering after a crash — the per-process temp directory under /tmp/initrunner-ssh-* is cleaned up on normal shutdown. If a hard kill leaves one behind, delete the directory or run ssh -O exit <host> once.

On this page