Skip to content

SSH Workspace

Run your agent’s commands and file edits on another machine over SSH, using your own ssh client and its configuration.

Source

While Pydantic AI Harness is on 0.x releases, the API may change between minor releases; when it does, deprecation warnings and release-note migration guidance tell you (or your agent) exactly how to upgrade. See the version policy.

Install

Terminal
pip install "pydantic-ai-harness[anthropic]"

No extra is needed for SSH itself: the capability runs the ssh client already on your machine. The anthropic extra is there because the examples use an Anthropic model; swap it for your model provider’s extra.

Quick start

from pydantic_ai import Agent
from pydantic_ai_harness import Coder, SSHWorkspace

agent = Agent(
    'anthropic:claude-opus-5-5',
    capabilities=[SSHWorkspace('dev@build-box', working_dir='/srv/app', env={'UV_OFFLINE': '1'}), Coder()],
)
result = agent.run_sync('Run the test suite and fix the first failure.')

Coder’s shell and file tools now run on build-box, as the user you log in as, with that user’s full authority on the host. To confine the commands, wrap the capability in BubblewrapSandbox.

working_dir defaults to the login directory, and a relative one starts there. The directory must already exist. Commands get the remote login environment plus env=. For a single run, pass workspace=SSHWorkspaceBackend('dev@build-box') to agent.run instead.

Logging in

Logging in needs a key; passwords aren’t supported. ssh runs with BatchMode=yes, so a host that asks for a password or a key passphrase fails right away instead of hanging the run, and ssh_args= can’t turn prompts back on. Use a key without a passphrase, such as ssh_args=['-i', key_path] or IdentityFile in your SSH configuration, or load a key that has one into ssh-agent with ssh-add. Set the user in the destination (dev@build-box), with User in your SSH configuration, or with ssh_args=['-l', 'dev']. Your SSH configuration (~/.ssh/config) also supplies ports and jump hosts.

How commands run

Every operation opens an SSH connection, and file operations run as shell commands on the host, so the host needs a POSIX sh and the usual file utilities. Turn on connection sharing in your SSH configuration (ControlMaster auto with a ControlPersist time) to make them fast.

A host that can’t be reached, a missing working directory, or a connection lost mid-command raises WorkspaceUnavailableError. The exit code is the command’s own, even when it is 255, which ssh also uses for its own errors.

On a timeout or cancellation, a second connection stops the command’s processes on the host; a command that detached into a session of its own, like a Shell background job, keeps running.

A command that leaves a background process holding its output open, such as server & without redirecting the server’s output, doesn’t return until that process exits, because sshd waits for the output to close: redirect it, as in server > server.log 2>&1 &.

Reattach later

The ref is WorkspaceRef(provider='ssh', id='dev@build-box:/srv/app'), available from construction. SSHWorkspace only accepts a reference to its own host and directory, so a ref in message history can’t point it at another machine. SSHWorkspace(...).backend(ref) builds the backend for such a ref without connecting, and raises ValueError for any other.

Security

SSHWorkspace has the full authority of the remote user. Neither working_dir nor a FileSystem root jails shell commands. Do not pass your local environment to the host: choose the variables the command needs with env=, which is kept out of the capability’s repr.

env= values travel inside the command that ssh sends, so while a command runs they are visible in process listings (ps) to other users on both this machine and the host. Don’t put secrets in env= on a shared machine: keep them in a file only the remote user can read, or in the remote user’s login environment.

Platforms

The machine running the agent must be POSIX (Linux or macOS), like LocalWorkspace: constructing SSHWorkspace on Windows raises NotImplementedError. The host needs a POSIX sh and the usual file utilities.

Telemetry

SSHWorkspace emits no spans of its own. Core’s instrumentation records the workspace on the agent run span as pydantic_ai.workspace.provider (ssh) and pydantic_ai.workspace.id (the host and directory, recorded even without include_content, because it identifies the environment rather than content), and each command or file operation a tool makes runs inside that tool call’s span.

API reference

SSHWorkspace

Bases: AbstractCapability[AgentDepsT]

Gives runs a workspace on a remote host over SSH, using your ssh client and its configuration.

Commands run as the remote user, with that user’s full authority on the host. Wrap it in BubblewrapSandbox to sandbox them there.

from pydantic_ai import Agent
from pydantic_ai_harness import SSHWorkspace

agent = Agent('anthropic:claude-opus-5-5', capabilities=[SSHWorkspace('dev@build-box', working_dir='/srv/app')])

It declines a ref for any other host or directory, so a ref in message history can’t point it elsewhere.

Attributes

destination

The host, as you’d pass it to ssh: 'user@host', a Host alias, or 'ssh://user@host:port'.

Type: str

working_dir

Where commands start and relative paths resolve on the host; defaults to the login directory.

Type: str | None Default: None

read_only

Whether to wrap the workspace in a ReadOnlyWorkspace.

Type: bool Default: False

env

Environment variables for every command, on top of the remote login environment.

Type: Mapping[str, str] | None Default: field(default=None, repr=False)

ssh_args

Extra ssh arguments, such as ['-i', key_path]; prefer your SSH configuration where you can.

Type: Sequence[str] Default: ()

id

Fixed, so a later SSHWorkspace replaces an earlier one whole; pass distinct ids to keep both.

Type: str | None Default: 'ssh_workspace'

Methods

backend
def backend(ref: WorkspaceRef) -> SSHWorkspaceBackend

Attach to this capability’s host and working directory by ref, without connecting.

Returns

SSHWorkspaceBackend

Raises
  • ValueError — If ref is for another host or working directory; this capability never redirects commands to a host it wasn’t configured with.

SSHWorkspaceBackend

Bases: WorkspaceBackend, SupportsCommands

Run commands on a remote host with the system’s OpenSSH ssh client.

Authentication, host keys, ports and jump hosts come from your SSH configuration (~/.ssh/config) and agent; ssh never prompts, so a missing key fails instead of waiting for a password. File operations run as shell commands on the host (see Writing a backend), which needs a POSIX sh there. The remote directory is the environment: the first operation raises WorkspaceUnavailableError if it is missing or the host can’t be reached. On a timeout or cancellation, a second connection stops the command’s process group on the host. A background process that keeps the command’s output open holds the call until it exits, as sshd waits for the output to close.

Constructor Parameters

destination : str

The host, as you’d pass it to ssh: 'user@host', a Host alias from your SSH configuration, or 'ssh://user@host:port'.

working_dir : str | None Default: None

Where commands start and relative paths resolve, on the remote host; a relative path starts in the login directory, which is the default.

env : Mapping[str, str] | None Default: None

Environment variables for every command, on top of the remote login environment; the per-call env goes on top.

ssh_args : Sequence[str] Default: ()

Extra ssh arguments, such as ['-i', key_path], placed before the destination.

Attributes

ref

WorkspaceRef(provider='ssh', id='<destination>[:<working_dir>]'), available from construction.

Type: WorkspaceRef