Skip to content

Claude Managed Agents

A Claude Managed Agent handing tool work to an isolated Sprite sandbox

Claude Managed Agents let Anthropic run the agent loop and the model while the agent’s tools (the filesystem it reads and writes, the commands it runs) execute in a sandbox you control. Point a Managed Agent at a self-hosted environment and you run each session inside a Sprite, so the agent’s code, filesystem, and network egress never leave your infrastructure.

A self-hosted environment is a work queue. When a session is assigned to it, Anthropic enqueues a work item. A worker you run claims the item and starts a per-session Sprite that runs Anthropic’s tool runner. Tool inputs and outputs flow back to Anthropic so the model can see results; everything the tools touch stays in the Sprite.

ANTHROPIC RUNS THISYOU RUN THISClaude Platformagent loop + modelWork queuesession work itemsWorkerpolls · claims work>_Spritestateful · checkpointed1enqueues2claims3launches4tool calls & resultseverything the tools touch stays

The runner authenticates with an environment key, the only credential it needs for both the control plane and the per-session calls. Your organization API key never reaches the Sprite.

You need:

  • A Claude API key (ANTHROPIC_API_KEY) to create the environment and start sessions. It stays off the Sprite; the worker and runner use only the environment key.
  • A Sprites API token (org-slug/org-id/token-id/token-value). Export it as SPRITE_TOKEN.
  • An Anthropic agent. Create one with the Quickstart and note its agent ID.
  • A worker runtime on the worker host: Python 3.10+ with the anthropic and httpx packages, or Node 22+ with @anthropic-ai/sdk and tsx.
Terminal window
curl -sS https://api.anthropic.com/v1/environments \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{"name": "sprites", "config": {"type": "self_hosted"}}'

Note the returned environment ID (env_…).

In the Console, open the environment and click Generate environment key. Export it alongside the environment ID on the worker host:

Terminal window
export ANTHROPIC_ENVIRONMENT_ID="env_..."
export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."

The worker creates a Sprite, uploads Anthropic’s provider-agnostic runner, installs the SDK, and launches it as a Sprite service rather than a detached nohup process. A service is supervised: the Sprite owns it, so it survives beyond the request that created it and is restored on wake, neither of which a process backgrounded inside a single request can be counted on to do. Because a service that exits is otherwise restarted, the one-shot worker stops itself on completion so it is not brought back up.

The ANTHROPIC_* variables are written to a file the service sources rather than passed as service arguments, so the environment key never appears in process listings. The service deletes the file right after sourcing it, so the key isn’t left on the Sprite’s disk.

sprite_sandbox.py
import os
import httpx
API = os.environ.get("SPRITES_API_URL", "https://api.sprites.dev")
HEADERS = {"authorization": f"Bearer {os.environ['SPRITE_TOKEN']}"}
RUNNER_PATH = "/root/sandbox_runner.py"
# Anthropic's provider-agnostic runner. One call is the whole worker: it builds
# the per-session tool context, downloads skills, runs the tool loop, and posts
# results back. See the cookbook for the full file.
RUNNER_SRC = '''import asyncio, logging, os, sys
from anthropic import AsyncAnthropic
# The worker reports its lifecycle only through stdlib logging; send it to
# stdout so failures surface in the service log
# (/.sprite/logs/services/agent-runner.log).
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
async def main():
key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
async with AsyncAnthropic(auth_token=key) as client:
# handle_item() services the single already-claimed item, reading
# ANTHROPIC_WORK_ID / ANTHROPIC_SESSION_ID / ANTHROPIC_ENVIRONMENT_ID /
# ANTHROPIC_ENVIRONMENT_KEY from the env (written to runner.env below).
await client.beta.environments.work.worker(
environment_key=key, workdir="/workspace", unrestricted_paths=True
).handle_item()
asyncio.run(main())
'''
def _sq(v: str) -> str:
return "'" + v.replace("'", "'\\''") + "'"
def _name(session_id) -> str:
return f"claude-agent-{session_id.lower().replace('_', '-')[:40]}"
def find_live(session_id) -> str | None:
# Return the Sprite's name when one for this session already exists and is
# serving, so redelivered work is a no-op instead of re-provisioning it.
c = httpx.Client(base_url=API, headers=HEADERS, timeout=30)
name = _name(session_id)
resp = c.get(f"/v1/sprites/{name}")
if resp.status_code == 404:
return None
resp.raise_for_status()
return name if resp.json().get("status") in ("running", "warm") else None
def spawn(session_id, *, environment_id, work_id, environment_key) -> str:
name = _name(session_id)
c = httpx.Client(base_url=API, headers=HEADERS, timeout=300)
# Tolerate the Sprite already existing (reuse it).
created = c.post("/v1/sprites", json={"name": name, "wait_for_capacity": True})
if created.status_code not in (200, 201, 409):
created.raise_for_status()
c.put(f"/v1/sprites/{name}/fs/write",
params={"path": RUNNER_PATH, "workingDir": "/"},
content=RUNNER_SRC.encode()).raise_for_status()
# Install the SDK and create the workspace. This runs on every new Sprite;
# the checkpoint note at the end of the guide covers cutting the cost.
exec_resp = c.post(f"/v1/sprites/{name}/exec", params=[
("cmd", "bash"), ("cmd", "-c"),
("cmd", "mkdir -p /workspace && python3 -m pip install -q anthropic")])
exec_resp.raise_for_status()
# Exec streams framed output ending in an exit frame, 0x03 then the exit
# code; a nonzero code means the install failed (e.g. PyPI is blocked).
body = exec_resp.content
code = body[-1] if len(body) >= 2 and body[-2] == 0x03 else 0
if code != 0:
raise RuntimeError(f"sprite {name}: runner setup failed (exit {code})")
# The env contract Anthropic's worker reads. The environment key is the
# runner's only credential.
env = {
"ANTHROPIC_BASE_URL": os.environ.get("ANTHROPIC_BASE_URL", "https://api.anthropic.com"),
"ANTHROPIC_ENVIRONMENT_KEY": environment_key,
"ANTHROPIC_SESSION_ID": session_id,
"ANTHROPIC_ENVIRONMENT_ID": environment_id,
"ANTHROPIC_WORK_ID": work_id,
}
env_file = "".join(f"{k}={_sq(v)}\n" for k, v in env.items())
c.put(f"/v1/sprites/{name}/fs/write",
params={"path": "/root/runner.env", "workingDir": "/"},
content=env_file.encode()).raise_for_status()
# The service registers a Task to hold the Sprite active for the session,
# heartbeats it (5m expiry refreshed every 60s) while the runner works, then
# drops it and self-stops when the runner exits. A crashed runner leaves the
# task to expire on its own, so the Sprite is never held open indefinitely.
cmd = (
"set -a; . /root/runner.env; set +a; rm -f /root/runner.env; "
"sprite-env curl -X POST /v1/tasks -d '{\"name\": \"agent-runner\", \"expire\": \"5m\"}' >/dev/null 2>&1; "
"( while true; do sprite-env curl -X PUT /v1/tasks/agent-runner -d '{\"expire\": \"5m\"}' >/dev/null 2>&1; sleep 60; done ) & heartbeat=$!; "
f"python3 {RUNNER_PATH}; "
"kill \"$heartbeat\" 2>/dev/null || true; "
"sprite-env curl -X DELETE /v1/tasks/agent-runner >/dev/null 2>&1 || true; "
"sprite-env services stop agent-runner >/dev/null 2>&1 || true")
c.put(f"/v1/sprites/{name}/services/agent-runner",
json={"cmd": "bash", "args": ["-lc", cmd], "needs": []}).raise_for_status()
return name

The worker claims sessions from the queue and calls spawn() for each. Choose the always-on poller (only needs outbound HTTPS) or a webhook (no idle poller, needs a public endpoint).

# sprites_poller.py (always-on)
import asyncio, os, anthropic
from sprite_sandbox import find_live, spawn
async def main():
env_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
client = anthropic.AsyncAnthropic(auth_token=key)
async for work in client.beta.environments.work.poller(
environment_id=env_id, environment_key=key,
reclaim_older_than_ms=2000, auto_stop=False,
):
# Filter to session work and skip other item types (for example platform
# health checks); each session item carries the id to hand to a Sprite.
if work.data.type == "session":
try:
# Skip spawn when a live Sprite is already serving this session,
# so redelivered work doesn't re-provision it.
find_live(work.data.id) or spawn(
work.data.id, environment_id=env_id,
work_id=work.id, environment_key=key)
except Exception as e:
# One bad item must not kill the always-on worker.
print(f"[poller] failed work={work.id}: {type(e).__name__}", flush=True)
asyncio.run(main())

auto_stop=False because each item is handed to a Sprite that owns its own stop; the poller must not release the lease out from under it. For the webhook variant, verify the session.status_run_started delivery, then drain the queue with poller(drain=True, auto_stop=False) and call spawn() the same way.

Terminal window
pip install anthropic httpx
python sprites_poller.py

Create a session targeting the environment, then send the agent a turn. The worker claims it, starts a Sprite, and the agent’s tools run inside it.

import anthropic
client = anthropic.Anthropic() # uses ANTHROPIC_API_KEY
session = client.beta.sessions.create(
agent="agent_...", environment_id="env_...",
)
client.beta.sessions.events.send(session.id, events=[{
"type": "user.message",
"content": [{"type": "text", "text": "Create README.md describing this project."}],
}])

The file the agent writes lands on the Sprite’s disk, where it persists for the Sprite’s lifetime; relative paths resolve to /workspace, though an agent given no explicit path may write an absolute one elsewhere. /workspace is Anthropic’s system default working directory for tool execution, and the runner passes workdir="/workspace" to match it, so that workdir is what scopes and permits /workspace access. unrestricted_paths=True is what additionally lets the file tools reach paths outside the workdir, notably /mnt/session/outputs, where Anthropic’s harness normally directs final deliverables (here just a directory on the Sprite’s disk).

Each session gets its own claude-agent-* Sprite, and those Sprites outlive the session. An idle Sprite pauses, which stops compute billing, but its filesystem persists and keeps accruing storage cost until you delete it, so a busy environment builds up a fleet of paused Sprites. Reap each one once its session finishes, on the session-end webhook or with a periodic sweep, by calling DELETE /v1/sprites/{name}; copy anything you need out of /workspace first (for example via the filesystem API), because deleting a Sprite deletes its checkpoints too.

Most failures here surface either as a queue that never drains or as a Sprite service that exits early. Check the service log first; it usually tells you which side broke.

SymptomLikely causeCheck / fix
Session created, but no Sprite ever appearsNo worker is claiming the work item, either the poller isn’t running or the webhook isn’t registered, or the item is stuck from an earlier failed deliveryCheck the queue with GET /v1/environments/{id}/work/stats, authenticated with the environment key. Starting the poller, or a fresh webhook delivery, drains a backlogged item.
Sprite is created, but the session dies within secondsThe SDK install failed inside the Sprite (for example pip install anthropic blocked by a network policy on PyPI), so spawn() raises runner setup failed (exit N). Or the environment key is invalid, so the runner exits immediately.Both surface in the service log; see the row below for where to look.
Not sure where to look firstThe runner logs to stdoutRead /.sprite/logs/services/agent-runner.log inside the Sprite, via exec or the filesystem API.
Service shows failed (exit 143) or stopped right after a session finishesExpected. That’s the terminal state of the self-stop pattern the service command uses, not a failure.Nothing to fix; the service is intentionally not restarted once it stops itself.
A long session stalls mid-turnThe keep-alive task is missing or has expired, so the Sprite paused on its idle windowInside the Sprite, sprite-env curl -s /v1/tasks should show the agent-runner task with a future expires_at while a session is active. If it’s absent, the heartbeat loop died with the runner.
The same work item is delivered or claimed twiceExpected during lease reclaim or webhook retriesfind_live() makes redelivery a no-op for a session that’s already live, so duplicate log lines are harmless.
  • Self-hosted sandboxes: Anthropic’s reference for the worker contract.
  • Services: supervised long-running processes inside a Sprite.
  • Checkpoints: snapshot a prepared Sprite to skip cold-start setup.