Run Claude Managed Agents on CreateOS Sandbox
With Claude Managed Agents, Anthropic runs the agent loop: the model, the context, and the choice of which tool to call next. Anthropic does not run the tool calls themselves. You supply the machine that does.
CreateOS Sandbox is that machine. It boots in about a second, holds state for as long as you keep it alive, and lets you control its network egress from outside. Anthropic keeps the orchestration. You keep the execution boundary: the filesystem, the network, and the logs.
Two ways to wire it up
CreateOS supports both wirings below. They differ in where the agent's shell runs and which credentials you put inside the sandbox, so read the table before you pick.
| Self-hosted environment | Sandbox as a tool | |
|---|---|---|
| Who runs the agent's bash | a worker inside your sandbox | your orchestrator, in a sandbox it owns |
| Anthropic environment type | self_hosted | cloud |
| Agent's tools | the built-in agent toolset (bash, file edit, …) | one custom tool you define |
| Credentials on your host | org key + environment key | org key only |
| Who reaches out to whom | your worker polls Anthropic's work queue | Anthropic streams you a tool call; you answer it |
| Best for | full agentic coding, where the agent needs a real filesystem across a session | a narrow, auditable execution surface: the agent runs this, and nothing else |
Both keep the agent's execution inside CreateOS. They differ in how much of the agent's world lives there.
Self-hosted environment
The agent's own toolset runs in your sandbox. Anthropic hands work to a queue. A worker process inside the sandbox claims it, runs the tool call, and posts the result back. The agent gets a real machine: a shell, a network, and a filesystem it can build up over a session. None of that state leaves the sandbox.
Claude Managed Agent (Anthropic) ──▶ work queue ◀── worker polls
│
┌───────────────┴───────────────┐
│ CreateOS sandbox │
│ ant beta:worker poll │
│ → the agent's bash runs here │
└───────────────────────────────┘
Anthropic's self-hosted sandboxes guide describes this topology.
Sandbox as a tool
Anthropic hosts the agent loop (a cloud environment), and CreateOS becomes a
discrete tool the agent calls. The agent emits a run_command tool call. Your
orchestrator runs that command in a sandbox it owns and hands the output back.
Claude Managed Agent (Anthropic)
│ agent.custom_tool_use { command: "…" }
▼
your orchestrator (holds the CreateOS API key — the agent never sees it)
│ sandbox.runCommand("bash", ["-lc", command])
▼
CreateOS sandbox ──── output ────▶ user.custom_tool_result
Give the agent only the custom tool and omit the built-in agent toolset. CreateOS then becomes its single execution path, with no second shell to fall back on. The control-plane key stays on your host, and every command the agent runs passes through code you wrote, so you can log it, rewrite it, or refuse it.
Prerequisites
- A CreateOS Sandbox API key and control-plane URL.
- An Anthropic organization API key with the Managed Agents beta enabled (Console → API keys).
- For a self-hosted environment only: an environment and an environment
key (Console → Environments → Generate environment key). The environment key
authenticates the worker to its queue. It is a separate credential from the org
key and uses a different auth scheme. A
cloudenvironment needs neither, since nothing of yours polls the queue.
Set up a self-hosted environment
On the Anthropic side. Create a self_hosted environment and generate an
environment key for it.
TypeScript1const environment = await anthropic.beta.environments.create({2 name: "createos-workers",3 config: { type: "self_hosted" },4});
On the CreateOS side. Boot a sandbox with the environment credentials in its
environment, install the ant CLI, and start the worker as a daemon. It polls for
work until you tear the sandbox down.
TypeScript1const sandbox = await Sandbox.create({2 shape: "s-4vcpu-4gb",3 rootfs: "devbox:1",4 envs: {5 ANTHROPIC_BASE_URL: "https://api.anthropic.com",6 ANTHROPIC_ENVIRONMENT_ID: environment.id,7 ANTHROPIC_ENVIRONMENT_KEY: environmentKey, // never the org key8 },9});1011await sandbox.sh(`curl -fsSL "$ANT_RELEASE_URL" | tar -xz -C /usr/local/bin ant`);12await sandbox.sh(13 "nohup setsid ant beta:worker poll --workdir /workspace > /tmp/worker.log 2>&1 &",14);
Every session you point at that environment now executes inside the sandbox.
TypeScript1const agent = await anthropic.beta.agents.create({2 name: "createos-worker",3 model: "claude-haiku-4-5",4 tools: [{ type: "agent_toolset_20260401" }],5});6const session = await anthropic.beta.sessions.create({7 agent: agent.id,8 environment_id: environment.id,9});
Only the environment key sits inside the sandbox. The org key stays on your host, so a prompt injection that compromises the agent still cannot spend your organization's credits or read your other sessions.
Set up sandbox-as-a-tool
Create a cloud environment and an agent whose only tool is yours.
TypeScript1const environment = await anthropic.beta.environments.create({2 name: "createos-tool",3 config: { type: "cloud", networking: { type: "unrestricted" } },4});56const agent = await anthropic.beta.agents.create({7 name: "createos-tool-agent",8 model: "claude-haiku-4-5",9 system:10 "Your only way to run anything is the run_command tool, which runs a shell " +11 "command on a remote Linux machine. Report its output verbatim.",12 // No agent_toolset: the agent has no bash of its own, so every command it13 // wants to run must go through CreateOS.14 tools: [15 {16 type: "custom",17 name: "run_command",18 description: "Run a shell command on the remote Linux machine.",19 input_schema: {20 type: "object",21 properties: { command: { type: "string" } },22 required: ["command"],23 },24 },25 ],26});
Then run the loop. Open the event stream before you send the task, since any event emitted in between is lost, and answer every tool call.
TypeScript1const session = await anthropic.beta.sessions.create({2 agent: agent.id,3 environment_id: environment.id,4});56const stream = await anthropic.beta.sessions.events.stream(session.id);7await anthropic.beta.sessions.events.send(session.id, {8 events: [{ type: "user.message", content: [{ type: "text", text: task }] }],9});1011for await (const event of stream) {12 if (event.type === "agent.custom_tool_use") {13 const output = await runInSandbox(String(event.input.command));14 await anthropic.beta.sessions.events.send(session.id, {15 events: [16 {17 type: "user.custom_tool_result",18 custom_tool_use_id: event.id,19 content: [{ type: "text", text: output }],20 },21 ],22 });23 } else if (event.type === "session.status_idle") {24 // `requires_action` means the agent is idle *waiting on your tool result* —25 // the run is not over. Only a real stop reason ends it.26 if (event.stop_reason?.type === "requires_action") continue;27 break;28 }29}
Sandbox lifecycle: per session, or per call
runInSandbox is where you choose what the agent's world looks like.
One sandbox per session. Create it before the session starts, reuse it for every tool call, then destroy it at the end. State persists: a file the agent writes in one call is there in the next, so the agent can build something up over a conversation.
A fresh sandbox per call. Create, run, destroy, every time. Nothing carries between calls, so one command cannot contaminate the next, and any command's blast radius is a sandbox you have already thrown away. You pay a cold start per call, which on CreateOS measures 0.7–1.0 s end to end (create, run, destroy).
Pick per-call for untrusted or one-shot work, per-session for anything that needs to accumulate.
Why CreateOS
- Egress you control from outside the sandbox.
setEgresslocks a sandbox to an allowlist, and the host enforces those rules in-kernel, so code inside the sandbox cannot bypass them. The agent can reach your private service and still have no route to the public internet. - Hardware isolation. Each sandbox runs its own kernel rather than sharing one across containers, which puts a hardware boundary between a language model's output and your infrastructure.
- ~1 s cold start. A fresh sandbox per tool call costs 0.7–1.0 s, which keeps per-call disposal practical.
- State when you want it. Pause and resume a sandbox, snapshot it, or reattach a disk, so a session survives an idle user without burning compute.
- Your keys stay yours. In both topologies the CreateOS API key lives on your host. In the sandbox-as-a-tool topology every command the agent runs passes through your code first.
Gotchas
An empty tool result is rejected. Managed Agents rejects a tool result with no
content. Send something, even (no output).
Report command failures, do not throw them. Sandbox.sh throws on a non-zero
exit, which breaks the run when you call it inside a tool body. The agent needs to
see a failing command as a result it can reason about. Use runCommand and hand
the exit code back with the output.
TypeScript1const { result } = await sandbox.runCommand("bash", ["-lc", command]);2const output = `${result.stdout}${result.stderr}`.trim() || "(no output)";3return result.exit_code === 0 ? output : `exit ${result.exit_code}\n${output}`;
Hold the stream open. In the sandbox-as-a-tool topology the agent blocks on your tool result. If your orchestrator drops the stream mid-call, the session strands. A single-shot script works as written. A long-lived service needs reconnect handling.
session.status_idle does not mean the run is over. It fires with
stop_reason.type: "requires_action" while the agent waits on you. Break only on
a real stop reason.
Anthropic does not stage files for you. A self-hosted sandbox starts empty,
with no repo and no session files. Pass what the agent needs through
session.metadata and have your worker fetch it.
Working examples
All five run end to end against a live control plane.
| Example | Topology | What it shows |
|---|---|---|
| 36: self-hosted agent worker | self-hosted | one persistent sandbox running the worker |
| 37: sandbox per session | self-hosted | a fresh sandbox and worker per session |
| 49: egress-locked worker | self-hosted | the agent reaches a private service but cannot exfiltrate |
| 50: sandbox as a tool, per session | sandbox-as-a-tool | one sandbox for the session; state survives between tool calls |
| 51: sandbox as a tool, per call | sandbox-as-a-tool | a disposable sandbox per call; the same task, and the state is gone |
Examples 50 and 51 run the same two-step task, writing a file and then reading it back in a separate tool call, so you can watch the two lifecycles diverge.