Workspaces and Isolation
A workspace is the isolated execution environment in which one agent run operates. It bundles together a filesystem directory, a reachable harnessd HTTP endpoint, and (for some backends) managed git state. Workspaces are set up and torn down by the orchestration layer — the agent loop itself never manages its own workspace.
Choosing a workspace backend lets you control what each run can see and modify: its own branch of a repo, a fresh Docker container, a remote Hetzner VM, or simply a directory on the current host.
Workspace choice is separate from the permission model. Even a worktree or container workspace runs with the default {sandbox: unrestricted, approval: none} unless you also set a permissions block in your run request or profile. Picking an isolated workspace backend does not automatically sandbox the agent's filesystem or approval behavior. See Tools and Permissions for details.
Selecting a workspace for a run
Pass workspace_type in the POST /v1/runs request body:
{
"prompt": "refactor the auth module",
"workspace_type": "worktree"
}
Valid values are "local", "worktree", "container", and "vm". An empty string means no workspace is provisioned — the run executes in the calling process.
If workspace_type is absent from the request, the runner falls back to the isolation_mode field of the named profile (if one is set). Profile isolation_mode accepts "none", "worktree", "container", and "vm". Only "worktree", "container", and "vm" actually trigger provisioning — "none" and an empty string both mean no provisioning. Note that "local" is not a valid profile isolation mode: setting it in a profile is treated as no preference and results in no provisioning.
The four backends
Workspace lifecycle and events
When a workspace_type is set, the runner goes through a three-phase lifecycle before the agent loop starts:
- Provision — calls
workspace.New(ctx, wsType, opts), which internally callsProvision. If provisioning fails, the run ends immediately. - System prompt injection — after provisioning, the system prompt is re-resolved with the workspace path so that any
AGENTS.mdfile in the workspace directory is picked up. - Destroy — on run completion, failure, or cancellation,
ws.Destroyis called and the workspace is torn down.
Each phase emits a corresponding SSE event on GET /v1/runs/{id}/events:
| Event type | String value | Payload fields |
|---|---|---|
EventWorkspaceProvisioned | workspace.provisioned | workspace_type, workspace_path |
EventWorkspaceDestroyed | workspace.destroyed | workspace_type, workspace_path, error (only on destroy failure) |
EventWorkspaceProvisionFailed | workspace.provision_failed | workspace_type, error |
These events are only emitted when workspace_type is non-empty. A workspace.provision_failed event is always followed immediately by run.failed.
// Example workspace.provisioned payload
{
"workspace_type": "worktree",
"workspace_path": "/home/user/myrepo-subagents/run-abc123"
}
Passing configuration and secrets
Every workspace backend accepts two optional configuration surfaces:
| Field | What it is | Security note |
|---|---|---|
opts.Env | Map of environment variables injected into the workspace | Use this for secrets (API keys, tokens) |
opts.ConfigTOML | TOML string written to harness.toml in the workspace root (mode 0600) | Never put secrets here — the file persists on disk |
API keys such as OPENAI_API_KEY and ANTHROPIC_API_KEY are always passed via opts.Env, never written to harness.toml. The orchestration layer (symphd) enforces this pattern automatically.
harness.toml is written to disk inside the workspace. Secrets placed in ConfigTOML will appear on the filesystem and may be captured in logs or git history. Always use opts.Env for credentials.
Warm workspace pools
For workloads that provision many short-lived workspaces (such as benchmark suites), the Pool type pre-provisions a configurable number of slots in the background and hands them out via lease/return semantics. This eliminates cold-start latency on each run.
The symphd orchestrator exposes this as workspace_type: pool with a pool_size setting (default: 3) and pool_workspace_type specifying the inner backend (default: "container").
Pool workspaces are not registered in the default workspace registry — they require a configured Pool instance.
Quick reference: choosing a backend
| Need | Recommended backend |
|---|---|
Local dev, single agent, harnessd already running | local |
| Parallel agents on the same repo, no Docker required | worktree |
| Full filesystem isolation, reproducible environment | container |
| Long-running work on a cloud VM | vm (with awareness of the tool routing limitation) |
| High-throughput benchmark or eval suite | pool (via symphd) |
Next steps
- Tools and Permissions — configure sandbox behavior and approval policies that apply inside any workspace.
- Events — subscribe to
workspace.provisionedandworkspace.destroyedto observe workspace lifecycle in real time. - Skills, Profiles, and Subagents — set
isolation_modein a profile to select a workspace backend per task type without changing caller code.