Skip to content
Changelog

Run ClaudeAgent and CodexAgent in a sandbox that survives losing the host

October 16, 20262 min read

Agno's ClaudeAgent(sandbox=...) and CodexAgent(sandbox=...) build their own coordinator from the agent's PostgresDb on first use, so a provider and a database are all the setup a sandboxed coding agent needs.

from agno.agents.claude import ClaudeAgent
from agno.db.postgres import PostgresDb
from agno.sandbox.docker import DockerSandbox
 
agent = ClaudeAgent(
    id="coder",
    model="claude-sonnet-4-6",
    permission_mode="bypassPermissions",
    db=PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai"),
    sandbox=DockerSandbox("agno-sandbox:local", network="isolated"),
)
 
agent.run("Write hello.py that prints hello and run it.", session_id="sandbox-1")
agent.run("What file did you create in this session?", session_id="sandbox-1")

The sandbox is a computer that belongs to one session. Claude Code or Codex runs there as a whole process, with the workspace and its own conversation files next to it, and after every turn Agno saves both as a checkpoint. Docker and Amazon Bedrock AgentCore ship as providers in this release. A lost container, host or replica costs nothing past the last completed turn, because the next turn restores from that checkpoint wherever it runs.

One turn, start to finish

Each turn claims the session through a lease row in <session_table>_sandbox_bindings, a table Agno creates in the agent's Postgres on first use. It starts a computer restored from the last committed checkpoint, runs the turn, captures and verifies a new checkpoint, and then publishes the run row and the recovery point in one transaction. A cancelled or failed turn publishes nothing, so the previous checkpoint stays the recovery point. When the previous turn left its computer warm, the next turn resumes it and skips the restore.

The per-turn cycle of a sandboxed ClaudeAgent or CodexAgent. Lease claims the session through a lease row in the agent's Postgres. Restore starts a fresh computer from the last committed checkpoint, with the workspace and native conversation files. Run checks the committed native id first, then runs Claude Code or Codex inside. Capture and verify takes a new checkpoint and checks it in the checkpoint store. Publish writes the run row and the recovery point in one transaction. The next turn starts from that recovery point on any host or replica. A cancelled, failed or native_state_missing turn publishes nothing and the previous checkpoint stays the recovery point.

Refusing state it cannot verify

Every turn passes the runtime the native conversation id committed with the last checkpoint, the Claude session id or the Codex thread id. If the restored computer holds a different conversation or none, the runtime fails with native_state_missing before the model runs, and the coordinator treats that as a restore it can retry. A completed turn that reports no native conversation, or whose transcript could not be saved, publishes nothing either.

We drove the release's sandbox runtime directly, with a stand-in agent in place of Claude Code, once with the conversation restored and once with it gone.

# real output, stand-in agent in place of the model
# restore holds the conversation: model calls=['model']
{"type": "execution_finished"}
# restore lost the conversation: model calls=[]
{"type": "execution_error", "reason": "native_state_missing", "error": "Sandbox native state does not match the session: expected the native conversation native-1, found none"}

Settings that cross into the sandbox

Typed harness settings such as model, permission_mode or allowed_tools travel to the runtime as JSON. Settings that only mean something on the host fail when you build the agent, each one by name, and so does a database other than Postgres.

# real output
ClaudeAgent(..., sandbox=box, cwd="/srv/repo", retries=2)
ValueError: Not supported with sandbox=: cwd, retries. The sandbox owns the workspace, process and local files; configure retries on QueueConfig.
 
CodexAgent(..., sandbox=box, env={"FOO": "1"})
ValueError: Not supported with sandbox=: env. The sandbox owns the workspace, process and local files; configure retries on QueueConfig.
 
ClaudeAgent(..., db=SqliteDb(...), sandbox=box)
ValueError: A sandboxed agent requires a PostgresDb or AsyncPostgresDb, on the agent or its AgentOS

BaseExternalAgent gains two options that the sandbox runtime turns on for every turn. require_native_resume fails the run when the recorded native conversation cannot be resumed, and strict_persistence fails it when its run row cannot be written. The new agno[claude] and agno[codex] extras install claude-agent-sdk and openai-codex.

This entry ran with no model key and no container. The release's 243 runtime, checkpoint and sandbox= unit tests pass, and so do its 120 Postgres store tests for leases, staging and the atomic commit, on an embedded Postgres.

See the cookbook, and learn more about running Claude and Codex as agent harnesses in the documentation.

Frequently asked questions

A PostgresDb or AsyncPostgresDb as the agent's db and a provider such as DockerSandbox("agno-sandbox:local", network="isolated") as sandbox=. The agent builds its coordinator from that database on first use, and agno[claude] or agno[codex] installs the harness SDK.

Nothing past the last completed turn is lost. The next turn, on any replica that shares the Postgres and the checkpoint storage, starts a computer restored from the last committed checkpoint, which holds the workspace and the harness's own conversation files.

The restored computer did not hold the Claude session or Codex thread committed with the last checkpoint. The runtime stops before the model is called, nothing is published, and the coordinator can retry the restore.

Host-only settings such as cwd, retries, env, codex_bin, keep_uploads and the native option objects. Agno names each one in a ValueError when you build the agent, and points retries at QueueConfig.

Shipped around the same time