USE CASES/Coding Agents

#Run Coding Agents With Supervised Sessions

Use the builtin coding-agent template when an application needs to run Codex, Claude Code, OpenCode, or Pi inside the same Sandbox0 execution environment. The template installs all four native CLIs and declares /workspace as its writable mount point.

Each coding agent remains a native subprocess. Session Supervisor owns process lifetime, stdin/stdout transport, restart attempts, and the replayable event journal; it does not translate one harness protocol into another.

The template sets both the container's operating-system HOME and every supervised session's cwd to /workspace. A claim binds the selected workspace Volume there, so the repository and each harness's native home-relative state share the same persistent mount. For example, Codex uses /workspace/.codex, Claude Code uses /workspace/.claude, OpenCode follows XDG defaults below /workspace, and Pi uses /workspace/.pi/agent.

HarnessNative commandProtocol
Codexcodex app-server --stdioCodex JSON-RPC over JSONL
Claude Codeclaude -p --input-format stream-json --output-format stream-jsonClaude Code stream-json
OpenCodeopencode acp --cwd /workspaceAgent Client Protocol over nd-JSON
Pipi --mode rpcPi RPC over JSONL

The examples grant the selected coding agent broad control inside its sandbox. Keep model credentials narrow, restrict network policy for production workloads, and do not mount a developer home directory or host Docker socket into the sandbox.

Create The Environment#

The SDK snippets assume you already configured a client as shown in Get Started. They create one SandboxVolume for the complete workspace, then claim the builtin template. The examples leave ttl and hard_ttl unset.

go
workspaceVolume, err := client.CreateVolume(ctx, apispec.CreateSandboxVolumeRequest{ AccessMode: apispec.NewOptVolumeAccessMode(apispec.VolumeAccessModeRWO), }) if err != nil { log.Fatal(err) } sandbox, err := client.ClaimSandbox(ctx, "coding-agent", sandbox0.WithSandboxBootstrapMount(workspaceVolume.ID, "/workspace"), ) if err != nil { log.Fatal(err) } fmt.Println("Sandbox ID:", sandbox.ID)

The Volume is optional. If omitted, /workspace is a writable directory in the sandbox rootfs and follows that sandbox identity's checkpoint lifecycle. Use a Volume when the repository and native harness state must persist independently from the sandbox.

Do not bake provider credentials into the template image. For production, prefer egress auth, LLMProxy, or another narrowly scoped external model proxy. Session environment variables are returned as part of the session specification and should not be treated as a secret store.

Start A Native Harness#

Select one harness and create one pipe-based supervised session. These examples use Codex; replace harnessName with claude-code, opencode, or pi to select another native command from the same map.

go
type harnessSpec struct { Command []string Env apispec.ExecutionSessionSpecEnv } harnesses := map[string]harnessSpec{ "codex": { Command: []string{"codex", "app-server", "--stdio"}, Env: apispec.ExecutionSessionSpecEnv{ "HOME": "/workspace", }, }, "claude-code": { Command: []string{ "claude", "-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--replay-user-messages", "--permission-mode", "dontAsk", "--allowedTools", "Bash", "Edit", "Write", "Read", "Glob", "Grep", "WebFetch", "WebSearch", "Agent", }, Env: apispec.ExecutionSessionSpecEnv{ "HOME": "/workspace", }, }, "opencode": { Command: []string{"opencode", "acp", "--cwd", "/workspace"}, Env: apispec.ExecutionSessionSpecEnv{ "HOME": "/workspace", }, }, "pi": { Command: []string{"pi", "--mode", "rpc", "--approve"}, Env: apispec.ExecutionSessionSpecEnv{ "HOME": "/workspace", }, }, } harnessName := "codex" harness := harnesses[harnessName] session, err := sandbox.CreateSession(ctx, apispec.ExecutionSessionSpec{ Name: apispec.NewOptString(harnessName), Command: harness.Command, Cwd: apispec.NewOptString("/workspace"), Env: apispec.NewOptExecutionSessionSpecEnv(harness.Env), Lifecycle: apispec.NewOptExecutionSessionLifecycleSpec( apispec.ExecutionSessionLifecycleSpec{ Restart: apispec.NewOptExecutionSessionRestartSpec( apispec.ExecutionSessionRestartSpec{ Policy: apispec.NewOptExecutionSessionRestartPolicy( apispec.ExecutionSessionRestartPolicyOnFailure, ), }, ), RuntimeRecovery: apispec.NewOptExecutionSessionRuntimeRecoveryPolicy( apispec.ExecutionSessionRuntimeRecoveryPolicyRestart, ), }, ), Readiness: apispec.NewOptExecutionSessionReadinessSpec( apispec.ExecutionSessionReadinessSpec{ Type: apispec.NewOptExecutionSessionReadinessType( apispec.ExecutionSessionReadinessTypeProcess, ), }, ), }, &sandbox0.CreateSessionOptions{ IdempotencyKey: "coding-agent-" + harnessName + "-v1", }) if err != nil { log.Fatal(err) } fmt.Println("Session ID:", session.ID)

Use one active coding agent per workspace. Multiple supervised sessions can run in one sandbox, but concurrent writers can race on repository files and the Git index.

Exchange Native Messages#

All four commands use pipes, not a PTY. Write one LF-terminated native record at a time and fence the write to the current process attempt. The helper below accepts any JSON-compatible value; each harness section provides the native messages to send.

go
func writeJSONLine( ctx context.Context, sandbox *sandbox0.Sandbox, sessionID string, value any, ) error { session, err := sandbox.GetSession(ctx, sessionID) if err != nil { return err } attempt, ok := session.Attempt.Get() if !ok { return fmt.Errorf("session has no running attempt") } data, err := json.Marshal(value) if err != nil { return err } data = append(data, '\n') _, err = sandbox.WriteSessionInput(ctx, sessionID, apispec.ExecutionSessionInputRequest{ InputID: uuid.NewString(), ExpectedAttemptID: apispec.NewOptString(attempt.ID), DataBase64: apispec.NewOptString(base64.StdEncoding.EncodeToString(data)), }) return err }

Consume output through the resumable session event stream. Events contain base64-encoded byte chunks, and chunk boundaries are not native protocol record boundaries. Buffer stdout bytes until LF before parsing JSON. The complete Go, Python, TypeScript, and CLI event-stream examples are in Supervised Sessions.

Commit the last processed event seq outside the sandbox and reconnect with after. Put a native request ID in messages where the harness supports one. Sandbox0 can deduplicate an input submission, but it cannot guarantee that the subprocess consumed that input exactly once.

Drive The Selected Harness#

Use the native message shape for the command you selected. Send the initialization records first, wait for their responses, save the returned native thread or session ID, and then send the prompt record with that ID.

json
{"method":"initialize","id":1,"params":{"clientInfo":{"name":"sandpi","title":"SandPi","version":"0.1.0"}}} {"method":"initialized","params":{}} {"method":"thread/start","id":2,"params":{"cwd":"/workspace","approvalPolicy":"never","sandbox":"danger-full-access"}} {"method":"turn/start","id":3,"params":{"threadId":"<thread-id-from-response-2>","clientUserMessageId":"task-1","input":[{"type":"text","text":"Inspect this repository and run the relevant tests."}]}}

Codex#

Codex requires initialize followed by initialized once per app-server process. Save result.thread.id from thread/start. After a new process attempt, initialize again and call thread/resume with the saved thread ID. The outer Sandbox0 sandbox is the isolation boundary in this example, so Codex uses danger-full-access inside that boundary. See the upstream Codex app-server protocol.

Claude Code#

The first stdout record is normally a system message with subtype init; save its session_id. Subsequent records include user acknowledgements, assistant messages, tool activity, and a final result. To continue after replacing the supervised session, append --resume and the saved native session ID to the Claude command. See the upstream Claude Code headless documentation.

The builtin template currently runs supervised processes as root. Claude Code refuses bypassPermissions and its equivalent --dangerously-skip-permissions when running as root, so the example uses native dontAsk mode with an explicit coding-tool allowlist. Add project-specific MCP tools to that allowlist when needed; unlisted permission requests are denied instead of blocking a headless session.

Claude Code's package is not published under an open-source license. Review the license bundled with the package and Anthropic's applicable terms before publicly redistributing a derived template image. Use API-key or supported cloud-provider authentication for third-party products; do not proxy a user's Claude subscription login.

OpenCode#

OpenCode implements Agent Client Protocol. An ACP client must handle server requests such as permission decisions instead of assuming stdout only contains responses and notifications. After a new process attempt, initialize ACP again and call session/load or session/resume with the saved native session ID. See the upstream OpenCode ACP command and Agent Client Protocol.

Pi#

Pi RPC supports request IDs, state inspection, prompting, steering, follow-up, and abort without an additional adapter. Save the sessionFile or sessionId returned by get_state, then use Pi's native --session or --session-id option when creating a replacement supervised session. See the upstream Pi RPC protocol.

Switch Harnesses#

Stop and delete the current supervised session before starting another harness against the same sandbox.

go
_, err := sandbox.SetSessionDesiredState( ctx, session.ID, apispec.ExecutionSessionDesiredStateStopped, ) if err != nil { log.Fatal(err) } if _, err := sandbox.DeleteSession(ctx, session.ID); err != nil { log.Fatal(err) }

Then select the next entry from the harness map and create a new supervised session. The same /workspace Volume preserves both the repository and each harness's native home-relative state. Decide which metadata belongs in version control, and keep credentials and caches out of the repository.

Do not replace a Codex session specification with a Claude Code, OpenCode, or Pi command. A new supervised session keeps each event journal, process attempt history, and native protocol stream internally consistent.

Recovery Responsibilities#

Session Supervisor restores the process, not the harness connection. When an event reports a new attempt_id or runtime_generation:

  1. stop writing to the old attempt
  2. reconnect to the Sandbox0 event journal from the last committed seq
  3. perform the harness's native initialize handshake again
  4. resume the saved native thread or conversation ID where supported
  5. reconcile any request whose acceptance was ambiguous before retrying it

See Supervised Sessions for attempt fencing, event retention, reconnect cursors, and input-delivery semantics.