SSH Host Access
Route agent tool commands to a remote host via SSH — use host tools, remote build servers, macOS Xcode, or Windows .NET from inside the container
SSH host access lets the agent route tool commands to a remote host over SSH. The agent container doesn't need any runtimes installed — commands are executed on the remote machine and output is streamed back. Transparent shell wrappers intercept tool calls and forward them through ssh-exec.
Why SSH
- Host tools — use tools installed on the Docker host without mounting the Docker socket
- Remote build server — offload builds to a powerful machine or GPU server
- macOS Xcode — run
xcodebuild,swift,xcrunfrom a Linux container - Windows .NET — run
dotnet,msbuildfrom a Linux container via SSH to a Windows host - Resource-constrained host — when you cannot run sidecar containers
SSH host access works with both runners (docker and compose), since it's just environment variables injected into the agent container.
Profile Configuration
Add an ssh: block to your agent profile:
ssh:
host: 192.168.1.100 # IP or hostname of target machine
port: 22 # optional, default: 22
user: agent # optional, default: "agent"
key_path: ~/.ssh/id_rsa # optional, default: ~/.ssh/id_rsa
host_cwd: /projects/myapp # optional working directory on remote host
The runner automatically:
- Injects SSH parameters as environment variables into the container.
- Bind-mounts the key file (read-only) from host into the container at
/home/agent/.ssh/id_rsa. entrypoint.shdetectsSSH_HOSTand creates wrappers for any runtime commands not natively available.
SSH Profile Fields
| Field | Type | Default | Description |
|---|---|---|---|
ssh.host | string | — | Remote host address (required) |
ssh.port | number | 22 | SSH port |
ssh.user | string | agent | SSH username |
ssh.key_path | string | ~/.ssh/id_rsa | Path to private key on host machine. ~ is expanded. Mounted read-only at /home/agent/.ssh/id_rsa inside the container. |
ssh.host_cwd | string | Current directory | Working directory on the remote host. Supports ${VAR} interpolation. |
Injected Environment Variables
| Variable | Source | Description |
|---|---|---|
SSH_HOST | ssh.host | Remote host address |
SSH_PORT | ssh.port (default 22) | SSH port |
SSH_USER | ssh.user (default "agent") | SSH username |
SSH_KEY_PATH | Always /home/agent/.ssh/id_rsa | Path to the key inside the container (mounted from ssh.key_path) |
SSH_HOST_CWD | ssh.host_cwd | Working directory on the remote host |
How ssh-exec Works
ssh-exec is a shell script at /opt/sidecar/ssh-exec. When a wrapper invokes it:
- Reads
SSH_HOST,SSH_PORT,SSH_USER,SSH_KEY_PATH,SSH_HOST_CWDfrom the environment. - Shell-escapes all arguments with
printf '%q'. - Connects via SSH with
StrictHostKeyChecking=no(suitable for controlled environments). - Runs
cd <SSH_HOST_CWD> && <escaped command>on the remote host. - Returns stdout/stderr and exit code to the caller.
# What the wrapper does internally:
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -p 22 -i /home/agent/.ssh/id_rsa \
[email protected] \
"cd /projects/myapp && npm install"
Key Management
Generate a Dedicated Key
ssh-keygen -t ed25519 -f ~/.ssh/heretic-agent -N "" -C "heretic-agent"
ssh-copy-id -i ~/.ssh/heretic-agent.pub user@target-host
Profile Configuration
ssh:
host: target-host
user: myuser
key_path: ~/.ssh/heretic-agent
The runner expands ~ and binds the key at /home/agent/.ssh/id_rsa inside the container.
Set Correct Permissions
chmod 600 ~/.ssh/heretic-agent
chmod 700 ~/.ssh
Key Security
ssh-execuses-o StrictHostKeyChecking=no— appropriate for known controlled hosts (your own machines, CI environments). For stricter setups, overridessh-execin your custom image to supply aknown_hostsfile.- Keep the private key file permissions at
600on the host. - The key is mounted read-only (
:ro) inside the container. - For CI/CD environments, store the key in a secrets manager and reference it via the
secrets:mechanism.
Using Secrets for the Key Path
secrets:
SSH_PRIVATE_KEY_PATH: ~/.heretic/get-ssh-key-path.sh
ssh:
host: build-server.example.com
key_path: ${SSH_PRIVATE_KEY_PATH}
The script simply echoes the path:
#!/bin/bash
echo "/home/youruser/.ssh/heretic-agent"
Full Example: SSH to Local Docker Host
The most common pattern: the agent container runs on your machine, and SSH gives it access to tools installed on your host (outside Docker).
Prerequisites on Host Machine
# Install sshd if not running
# macOS: System Settings → Sharing → Remote Login
# Linux: sudo systemctl enable --now sshd
# Create agent user (or use your own account)
sudo useradd -m agent
sudo passwd agent
# Generate and authorize the key
ssh-keygen -t ed25519 -f ~/.ssh/heretic-agent -N "" -C "heretic-agent"
ssh-copy-id -i ~/.ssh/heretic-agent.pub agent@localhost
Get the Docker Bridge IP (Linux)
docker network inspect bridge --format '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
# Usually: 172.17.0.1
For macOS, use host.docker.internal as the hostname.
Profile
name: claude-ssh-host
image: ghcr.io/your-org/heretic-agent:latest
runner: docker
agent_type: claude
provider: anthropic
description: Claude with SSH access to Docker host for native tools
secrets:
ANTHROPIC_API_KEY: ~/.heretic/get-anthropic-key.sh
volumes:
- source: ${CWD}
target: /workspace
workdir: /workspace
interactive: true
tty: true
ssh:
host: host.docker.internal # macOS; use 172.17.0.1 on Linux
port: 22
user: agent
key_path: ~/.ssh/heretic-agent
host_cwd: ${CWD} # same project dir on host
Linux Host
On Linux, host.docker.internal may not resolve. Use the Docker bridge IP (172.17.0.1) or add --add-host=host.docker.internal:host-gateway to your Docker run command.
Run it:
heretic-cli run claude-ssh-host
Inside the container, any tool not found locally is proxied to the host:
brew list # runs on macOS host (via ssh-exec)
xcrun --version # runs on macOS host
dotnet --version # runs on Windows WSL2 host
Full Example: SSH to Remote Build Server
name: claude-remote-build
image: ghcr.io/your-org/heretic-agent:latest
runner: docker
agent_type: claude
provider: anthropic
description: Claude with SSH to remote GPU build server
secrets:
ANTHROPIC_API_KEY: ~/.heretic/get-anthropic-key.sh
volumes:
- source: ${CWD}
target: /workspace
workdir: /workspace
interactive: true
tty: true
ssh:
host: build.example.com
port: 22
user: ci
key_path: ~/.ssh/build-server-key
host_cwd: /home/ci/projects/myapp
Local Override for Different Project Path
Create .heretic/cli/claude-remote-build.yaml:
extends: claude-remote-build
ssh:
host_cwd: /home/ci/projects/different-project
Combining SSH and Sidecars
Sidecars and SSH can be combined. The priority is:
- Native command in agent container → used as-is
- Sidecar in
BUILD_SIDECARSfor this runtime →sidecar-exec - SSH configured (
SSH_HOSTset) →ssh-exec
Use case: Python sidecar for testing, SSH fallback for Go on the build server:
runner: compose
dind: true
env:
BUILD_SIDECARS: '{"python":"python-sidecar"}'
ssh:
host: build.example.com
user: ci
key_path: ~/.ssh/build-key
host_cwd: /home/ci/myapp
compose:
services:
python-sidecar:
image: python:3.12-slim
working_dir: /workspace
volumes:
- type: bind
source: ${CWD}
target: /workspace
command: ["/bin/sh", "-c", "pip install poetry pytest && sleep infinity"]
Result:
python3,pytest,poetry→ Python sidecargo,cargo,node, etc. → SSH to build server
Local Overrides for SSH
Local overrides (.heretic/cli/<profile>.yaml) follow three-layer merge with these rules:
ssh:— shallow merge: local keys win over global- To change only the
host_cwdper project, override just that field
Example: different SSH host per project
Global profile (~/.heretic/agents/claude-dev.yaml):
ssh:
host: default-build-host.internal
user: agent
key_path: ~/.ssh/heretic-agent
Project-local override (.heretic/cli/claude-dev.yaml):
extends: claude-dev
ssh:
host: this-project-server.internal
host_cwd: /home/agent/specific-project
See Local Overrides and Merge Rules for details.
Debugging
Test SSH Connectivity
From inside the agent container:
ssh -i /home/agent/.ssh/id_rsa \
-o StrictHostKeyChecking=no \
-p $SSH_PORT \
$SSH_USER@$SSH_HOST \
"echo connected && uname -a"
Manually Test ssh-exec
/opt/sidecar/ssh-exec npm --version
/opt/sidecar/ssh-exec python3 --version
Check Environment Variables
env | grep SSH_
Check Key Permissions
ls -la /home/agent/.ssh/id_rsa
# Should be -rw------- (600)
Check Generated Wrappers
ls /opt/sidecar/wrappers/
cat /opt/sidecar/wrappers/go
# #!/bin/bash
# exec /opt/sidecar/ssh-exec "go" "$@"
Enable Debug Logging
HERETIC_LOG_LEVEL=debug heretic-cli run my-profile
Reference
All SSH Profile Fields
| Field | Type | Default | Required | Description |
|---|---|---|---|---|
ssh.host | string | — | Yes | Remote host address |
ssh.port | number | 22 | No | SSH port |
ssh.user | string | agent | No | SSH username |
ssh.key_path | string | ~/.ssh/id_rsa | No | Path to private key on host machine. ~ expanded. |
ssh.host_cwd | string | Current dir | No | Working directory on the remote host. ${VAR} supported. |
SSH Options Used by ssh-exec
| Option | Value | Why |
|---|---|---|
StrictHostKeyChecking | no | Containers are ephemeral — no persistent known_hosts |
UserKnownHostsFile | /dev/null | Don't pollute known_hosts in disposable containers |
LogLevel | ERROR | Suppress SSH banners and warnings |
Next Steps
- Sidecars — Route commands to sidecar containers via docker exec
- Runners — Choose between docker, compose, and custom runners
- Secrets — Securely manage SSH keys and API tokens
- Local Overrides — Per-project SSH host overrides