Skip to content

Run agents on another machine

A binding can run its executor on another machine instead of the one Open Conduit runs on. Use one to give builds a bigger machine, to keep agents off a laptop, or to work an untrusted repository on a machine you can throw away.

Open Conduit stays where it is. For each run it copies the run’s workspace and its own files to the remote over SSH, runs the executor there, and copies back what the run changed. The run reaches its toolbelt over the same SSH connection, so every call is still checked by Open Conduit on your machine. Agents don’t need to know: they run git push and call the toolbelt as before.

The remote is a container provider, ssh, so it works with the shell and Claude Code executors and needs the same clone workspace a container does.

Files the run’s workspace, its run directory, the Work’s own home, and what you list in read and write, at the same paths as on your machine; nothing else of yours
Back on your machine what the run changed under write, the run directory and the Work’s home; never .git/config, .git/hooks or .git/info
Credentials the run’s toolbelt token, the secrets the binding selects (such as Claude’s sign-in), and through the toolbelt a GitHub token for the Work’s one repository, revoked when the run ends; never your SSH key or Open Conduit’s own token
Network whatever the remote’s own firewall allows
Machine pinned by its host key: a different key refuses every run

Open Conduit can’t limit the remote’s network or its resources. It still checks the hosts an executor says it needs against network.allow, but nothing on the remote enforces that list. Give the remote a firewall that does, and use a machine whose size is the limit.

This walks through a Linux machine, runner.example.com, with an account agent for the runs.

On the remote:

Terminal window
sudo useradd --create-home --shell /bin/bash agent

The account needs sh, setsid, ps, tar, find and env, which any Linux distribution has, and whatever the binding runs. For Claude Code, install claude where the account’s PATH finds it, such as /usr/local/bin.

For the toolbelt, the remote needs Open Conduit’s os on /usr/local/bin:/usr/bin:/bin. Take it from the Linux release archive for the remote’s architecture:

Terminal window
version=1.14.0 # what `open-conduit version` prints on your machine
arch=amd64 # or arm64
curl -fsSLO "https://github.com/open-conduit/open-conduit/releases/download/v$version/open-conduit_${version}_linux_$arch.tar.gz"
tar -xzf "open-conduit_${version}_linux_$arch.tar.gz" os
sudo install -m 0755 os /usr/local/bin/os

A run sees the same paths on the remote as on your machine, so nothing inside it needs rewriting. The account must be able to write Open Conduit’s state directory’s path there. With the default state directory, on a Mac whose user is alex:

Terminal window
sudo mkdir -p /Users/alex/.local/state/open-conduit
sudo chown -R agent: /Users/alex

Doctor names the path if it isn’t writable.

On your machine, make a key only Open Conduit uses, and authorise it for the account:

Terminal window
ssh-keygen -t ed25519 -N '' -f ~/.ssh/open-conduit
ssh-copy-id -i ~/.ssh/open-conduit.pub agent@runner.example.com

Open Conduit forwards the run’s toolbelt socket over the connection, so the remote’s sshd must allow AllowStreamLocalForwarding, as it does by default.

Open Conduit trusts one host key for the machine and no other. Read it from the remote itself, not over the network you are trying to trust:

Terminal window
cat /etc/ssh/ssh_host_ed25519_key.pub
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… root@runner

Keep the first two fields, ssh-ed25519 AAAA….

A remote binding declares a container with the ssh provider:

"bindings": [
{
"name": "agent",
"executor": "claude-code",
"connection": "github",
"requires": {"toolbelt": "mcp", "inject": true, "resume": true, "workspace": true},
"operations": ["read_context", "wait", "declare_outcome", "github.context", "github.issue.view",
"github.issue.comment", "github.pr.view", "github.pr.create", "github.pr.edit", "github.checks", "github.wait",
"github.git.credential"],
"secret_names": ["claude-token"],
"config": {"branch": "agent/issue-{number}", "workspace_mode": "clone",
"secret_env": {"claude-token": "CLAUDE_CODE_OAUTH_TOKEN"}},
"resource": {"provider": "git", "config": {"checkout": "/path/to/web", "mode": "clone"}},
"container": {"provider": "ssh", "config": {
"image": "runner.example.com@sha256:…",
"write": ["{workspace}"],
"network": {"allow": ["api.anthropic.com", "github.com"]},
"provider": {
"host": "runner.example.com",
"user": "agent",
"host_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…",
"identity": "~/.ssh/open-conduit",
"network": "host"
}
}}
}
]
Field What it does
image Pins the machine: <host>@sha256: and the SHA-256 of its host key. Leave it as above and run open-conduit config check, which gives the value to set.
write Paths copied to the remote and back. {workspace} is the run’s workspace. The run directory and the Work’s home always are.
read Paths only copied to the remote, such as ~/.gitconfig. Each run copies them again, so keep them small.
network.allow The hosts the executor may say it needs. It isn’t enforced on the remote.
provider.host, user, port Where to connect (port 22 unless you set one).
provider.host_key The remote’s host key, <type> <base64>.
provider.identity The key to sign in with. Without it, ssh uses your ~/.ssh/config and, if you grant it, your agent (below).
provider.network Must be host: you accept that the remote’s own network applies.

Leave limits out: the provider can’t enforce them and refuses a config with them. Your ~/.ssh/config still applies, so a ProxyJump to reach the remote works. Nothing there can change the host key Open Conduit checks.

The ssh provider runs as a package of its own, which sees only the variables you grant it. To sign in with your ssh agent rather than an identity, grant it the agent’s socket in the config’s settings:

"settings": {"extensions": {"ssh": {"pass_env": ["SSH_AUTH_SOCK"]}}}

Granting it there never gives it to the run, as a binding’s pass_env would. When the remote refuses the key and no agent is granted, the error says so.

Terminal window
open-conduit config check
open-conduit doctor

Doctor’s ssh check connects to each remote with its pinned key. It checks that the remote has the tools a run needs and can write the run’s records and your state directory’s path. It also says whether os is on the remote’s PATH. While a check fails, the remote’s bindings run nothing.

Stopping a run from the terminal UI, an interrupt and a run’s deadline reach the run on the remote. If the SSH connection drops while a run is going, Open Conduit stops the run on the remote, copies back what it changed and fails the run. A daemon restart doesn’t end a run whose helper is still going. A run that ended while nothing was watching is reported with its exit status. What it changed stays on the remote until the Work’s next run replaces it.

  • Every run copies its files again. A clone workspace is fine. A large read grant makes every run slow.
  • No cloud machines yet. Open Conduit doesn’t create or destroy the remote; point it at a machine you manage.
  • The remote is trusted with what it gets. Anyone with access to the account can read a running run’s files, its toolbelt token and its secrets. Give each binding an account of its own, on a machine nobody else uses.

For how it works, see the SSH provider.