Run Claude Managed Agents on CreateOS Sandbox
With Claude Managed Agents, Anthropic runs the agent loop — the model, the context, the decision about which tool to call next. It does not run the tool calls. Those need a machine, and that machine is yours.
CreateOS Sandbox is that machine: a hardware-isolated sandbox that boots in about a second, holds state for as long as you want it to, and whose network egress you control from the outside. Anthropic keeps the orchestration; you keep the execution boundary — its filesystem, its network, and its logs.
Two ways to wire it up
Most sandbox vendors document exactly one of these. CreateOS supports both, and the choice is a real one, so pick deliberately.
| 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 — the agent wants a real filesystem it can live in | a narrow, auditable execution surface: the agent may run 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 locally, and posts the result back. The agent gets a real machine — a filesystem it can build up over a session, a shell, a network — and none of it leaves the sandbox.
Claude Managed Agent (Anthropic) ──▶ work queue ◀── worker polls
│
┌───────────────┴───────────────┐
│ CreateOS sandbox │
│ ant beta:worker poll │
│ → the agent's bash runs here │
└───────────────────────────────┘
This is the topology Anthropic's self-hosted sandboxes guide describes, and the one every other sandbox vendor's integration page documents.
Sandbox as a tool
Anthropic hosts the agent loop (a cloud environment), and CreateOS is a discrete
tool the agent reaches for. The agent emits a run_command tool call; your
orchestrator runs it 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 — omit the built-in agent toolset — and CreateOS becomes its single execution path. There is no second shell for it to fall back to. The control-plane key stays on your host, and every command the agent runs passes through code you wrote, where 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 and is a different credential — and a
different auth scheme — from the org key. A
cloudenvironment needs neither: 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 is inside the sandbox. The org key stays on your host — so a prompt injection that fully 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 sending the task — events emitted in between would otherwise be 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, destroy it at the end. State persists: a file the agent writes in one call is there in the next, and the agent can build something up over a conversation.
A fresh sandbox per call — create, run, destroy, every time. Nothing persists, so nothing carries over: no cross-call contamination, no machine to clean up, and the blast radius of any single command is a sandbox that is already gone. The cost is 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; the rules are enforced in-kernel on the host, so code inside the sandbox cannot bypass them. An agent can reach your private service and still be unable to exfiltrate to the public internet — enforced, not merely prompted. - Hardware isolation. Every sandbox is its own kernel, not a shared-kernel container. That is the boundary you want between a language model's output and your infrastructure.
- ~1 s cold start. Fast enough that a fresh sandbox per tool call is a real option, not a theoretical one.
- State when you want it. Pause and resume a sandbox, snapshot it, or reattach a disk — a session can survive 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 the agent's every command passes through your code first.
Gotchas
An empty tool result is rejected. Managed Agents rejects a tool result with no
content. Always send something — (no output) is fine.
Report command failures, do not throw them. Sandbox.sh throws on a non-zero
exit. Inside a tool body that is wrong: a failing command is a result the agent
must see and reason about, not an orchestrator crash. 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 is fine as-is; 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 —
no repo, 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 — write a file, then read it back in a separate tool call — so the difference between the two lifecycles is something you can watch happen.