Anthropic's Claude Managed Agents (CMA) has a self-hosted mode. Claude keeps running the agent loop and reasoning on its side, while every tool call the agent makes — bash commands, file edits, code execution — runs inside infrastructure you control. The agent's filesystem, processes, and network egress never leave your boundary.
This is a complete, copy-pasteable walkthrough for making InstaVM that execution layer. By the end you'll have a public webhook endpoint registered with Anthropic and a fresh, isolated InstaVM microVM spinning up for every agent session.
There's one design decision worth calling out up front: the sandbox that runs untrusted agent code never holds a real API key. Third-party credentials are injected at the egress boundary from a vault, so the worker only ever sees placeholder strings. We'll get to why that matters.
The full source for this tutorial lives in the `claude-managed-agents` cookbook.
How CMA self-hosting works
A self-hosted environment is a work queue. When a session is assigned to it, Anthropic enqueues the session as a work item. Your job is to run a worker that claims work items, downloads the agent's skills, executes the tool calls, and posts the results back. Anthropic does the thinking; your worker does the doing.
There are two ways to wake the worker up:
- Always-on: a process that polls the queue continuously. Simple, but it burns resources while idle.
- Webhook-triggered: Anthropic calls your webhook on
session.status_run_started, and you spin a worker up on demand.
We'll build the webhook-triggered version. It has two moving parts:

- An orchestrator — a long-lived InstaVM service with a public URL. It receives the webhook, verifies the signature, and spawns a worker.
- A worker microVM — a short-lived, isolated VM created fresh for each
session. It runs
ant beta:worker poll, drains the queue, executes tools, and exits when the session goes idle.
Why we inject credentials at the egress boundary
Here's the part most sandbox integrations skip over. The worker needs an Anthropic environment key to authenticate to the work queue, and the agent often needs third-party API keys (OpenAI, Stripe, GitHub, …) to do anything useful.
The obvious approach is to put those secrets inside the sandbox — as
environment variables or files on disk. But the sandbox is exactly where
untrusted, agent-generated code runs. A prompt injection, a buggy tool call, or
a malicious dependency now sits on the same filesystem as your credentials. One
cat ~/.config/* and they're exfiltrated.
InstaVM routes every VM's outbound HTTPS through an egress proxy. You bind a
vault that maps a host (say api.openai.com) to a credential, and the proxy
substitutes the real value into the request at TLS write time. Your code ships
a placeholder:
OPENAI_API_KEY = "OPENAI_KEY" # placeholder string, never the real secret…and the real key materializes only on the wire, outside the VM. The agent can read every file and dump every environment variable it wants — there's nothing sensitive to find.
| Secrets-in-sandbox (typical) | InstaVM egress injection | |
|---|---|---|
| Where the key lives | Env var / file in the worker | Vault, outside the VM |
| Agent code can read it | Yes | No |
| Blast radius of a prompt injection | All bound credentials | Nothing on disk |
A note on the Anthropic environment key specifically: it's a scoped, revocable, queue-only credential (it can claim work for one environment — not your full account). You can inject it at egress like any other host credential if you prefer, or pass it to the worker directly; the high-value third-party provider keys are the ones you most want to keep out of the sandbox. This tutorial keeps all provider credentials out of the worker.
1. Prerequisites
# Anthropic API key (creates environments/agents/sessions from your host).
export ANTHROPIC_API_KEY="sk-ant-..."
# InstaVM CLI + SDK, authenticated.
pip install instavm
instavm auth login
# The ant CLI is installed *inside* the worker microVM, not here.2. The orchestrator
The orchestrator is a small FastAPI app. Deployed as an InstaVM service, it gets
a public share URL we register with Anthropic. It exposes four routes: / (a
status page), /health, /workers, and the all-important /webhook.
Verifying the webhook
Anthropic signs every delivery with the Standard Webhooks scheme using a
whsec_-prefixed signing key. The cleanest verification is the Anthropic SDK's
unwrap() helper, which checks the signature and rejects anything older than
five minutes:
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_WEBHOOK_SIGNING_KEY from env
def verify(body: str, headers: dict) -> dict:
# Raises if the signature is invalid or the payload is stale.
return client.beta.webhooks.unwrap(body, headers=headers)The cookbook also ships a small, dependency-free HMAC fallback so the whole thing
stays unit-testable offline. Verification fails closed: if the signing key
isn't configured, the endpoint returns 503 rather than trusting the payload.
The webhook handler
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.post("/webhook")
async def webhook(request: Request) -> JSONResponse:
body = await request.body()
try:
payload = verify_and_parse(body, request.headers, signing_key)
except WebhookError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
event_type, session_id = extract_session_event(payload)
if event_type != "session.status_run_started":
return JSONResponse({"ok": True, "ignored": event_type})
# Return 200 immediately; spawn the worker out of band.
dispatcher.schedule(session_id)
return JSONResponse({"ok": True, "scheduled": session_id})The handler returns 200 right away and schedules a background task. The
dispatcher debounces briefly so a burst of near-simultaneous webhooks collapses
into a single spawn per session.
3. Spawning a worker microVM
This is the core of the integration. For each session, the orchestrator creates a fresh InstaVM microVM, locks its egress down to an allowlist, and starts the poller. Note the environment key handed to the VM is a placeholder — the real value is injected at egress:
from instavm import InstaVM
def spawn_worker(session_id: str) -> str:
env = {
"ANTHROPIC_ENVIRONMENT_ID": ENVIRONMENT_ID,
"ANTHROPIC_ENVIRONMENT_KEY": "ANTHROPIC_ENV_KEY", # placeholder
"ANTHROPIC_SESSION_ID": session_id,
}
vm = InstaVM(
api_key=INSTAVM_API_KEY,
memory_mb=2048,
cpu_count=2,
env=env,
metadata={"cma_session_id": session_id, "role": "cma-worker"},
)
# Restrict outbound traffic to Anthropic's control plane (+ any hosts the
# agent legitimately needs). Everything else is blocked at the boundary.
vm.set_session_egress(
allow_package_managers=True,
allowed_domains=["api.anthropic.com"],
)
# Fire-and-forget: the poller runs until the session goes idle, then exits.
vm.execute_async(WORKER_BOOTSTRAP)
return vm.session_idThe worker bootstrap installs the ant CLI (once) and hands control to the
poller:
set -e
mkdir -p /workspace
if ! command -v ant >/dev/null 2>&1; then
ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')
curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v1.12.0/ant_1.12.0_linux_${ARCH}.tar.gz" \
| tar -xz -C /usr/local/bin ant
fi
exec ant beta:worker poll --workdir /workspace --max-idle 30sant beta:worker poll claims the session's queued work, downloads skills into
/workspace, executes the agent's bash and file tools inside the microVM, posts
results back to Anthropic, and shuts down after it's been idle for --max-idle.
Because the vault is bound to api.anthropic.com, the egress proxy swaps the
placeholder for the real environment key on the outbound request. The VM never
stored it.
Faster cold starts: instead of installing
antat boot, bake a snapshot with it pre-installed and create the worker from that snapshot. The first run downloadsant; every run after is instant.
4. Package it as an InstaVM service
InstaVM cookbooks are described by an instavm.yaml manifest. The key blocks are
app (the service port and health check), vault (the host whose credential is
injected at egress), and egress (the outbound allowlist):
schema_version: 2
kind: service
slug: claude-managed-agents
runtime: python-fastapi
app:
port: 8000
healthcheck_path: /health
share_public_default: true # gives us the public webhook URL
run:
start_command: python -m uvicorn app:app --host 0.0.0.0 --port 8000
vault:
required: true
hosts:
- api.anthropic.com # real key injected at egress on the workers
egress:
mode: allowlist
include_vault_hosts: true
allowed_domains:
- api.anthropic.com
secrets:
- name: AnthropicWebhookSigningKey
prompt: Anthropic webhook signing key (whsec_...)
env_name: ANTHROPIC_WEBHOOK_SIGNING_KEY
- name: AnthropicEnvironmentId
prompt: Self-hosted environment id (env_...)
env_name: ANTHROPIC_ENVIRONMENT_IDThe manifest is convenience, not magic — it's the same app.py, egress rules,
and vault binding you'd wire by hand, just declared once so instavm deploy
reproduces them every time.
5. Deploy and register
git clone https://github.com/instavm/cookbooks.git
cd cookbooks/claude-managed-agents
# Bind the vault so workers get the real Anthropic environment key at egress.
# The worker authenticates to the work queue with a Bearer token, so the
# binding is: host api.anthropic.com, auth_type=bearer, credential ANTHROPIC_ENV_KEY.
instavm vault setup .
# Deploy the orchestrator. Prompts for the webhook signing key and environment id,
# then prints the public share URL it provisioned — copy it; this is your <share-url>.
instavm deploy .
# → Service live at: https://<share-url> (e.g. https://claude-managed-agents-xxxx.instavm.site)
# Confirm it's live (and that <share-url> is the one deploy printed above).
curl https://<share-url>/health
# {"ok":"true","slug":"claude-managed-agents","active_workers":"0", ...}Throughout the rest of this guide,
is the public URL thatinstavm deployprinted when it provisioned the service. Everything Anthropic talks to — the webhook — lives under it.
Create the self-hosted environment
You can do this from the API:
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":"instavm","config":{"type":"self_hosted"}}'
# → {"id":"env_...", ...}Then, in the Claude Console,
open the environment and click Generate environment key (this step is
Console-only). Note the env_... id for the deploy prompt.
Register the webhook
In the Console under Manage → Webhooks, add an endpoint. The URL is the
that instavm deploy printed earlier, with /webhook appended:
URL: https://<share-url>/webhook
Subscribe: session.status_run_started (this event only)Copy the whsec_... signing key it shows you (once!) and re-deploy so the
orchestrator can verify deliveries.
Create an agent
ant beta:agents create <<'YAML'
name: InstaVM Assistant
model: claude-sonnet-4-6
system: You are a helpful assistant.
tools:
- type: agent_toolset_20260401
YAMLagent_toolset_20260401 gives the agent the built-in bash and file tools it runs
inside the worker.
6. Run a session
Create a session against that agent and environment, and send a message — something simple for the first smoke test:
ant beta:sessions create --agent <agent-id> --environment-id <env-id> --title "smoke"
ant beta:sessions:events send --session-id <session-id> \
--event '{type: user.message, content: [{type: text, text: "Use bash to echo it-works. Answer only with that text."}]}'Anthropic fires the webhook, the orchestrator spawns a worker microVM, and the
poller runs the echo inside the VM. Under the hood the worker opens the
session's event stream, replays the agent's pending tool calls, executes them in
the microVM, and posts the results back — every one of those requests
authenticating with the environment key the egress proxy injected. Watch it
happen:
curl https://<share-url>/workers
# {"workers":[{"session_id":"sess_...","status":"running","vm_id":"...","age_seconds":2.1}]}A healthy run ends with the session back at session.status_idle and the worker
microVM exiting on its idle timeout.
What you end up with
- Anthropic owns the loop. Reasoning, planning, and orchestration stay on Claude's side. You provide only the execution sandbox.
- Per-session isolation. Every agent session gets a fresh microVM with its own filesystem and an egress allowlist. Nothing bleeds between sessions.
- No naked secrets in the sandbox. Provider credentials are injected at the egress boundary from the vault. The worker — the place untrusted agent code actually runs — never holds them.
To give the agent access to another API, add the host to the worker's egress allowlist and bind a vault credential for it. The placeholder-in, real-key-at-the-edge pattern holds for every provider. That's the whole point: the agent gets to do its job, and your secrets never sit on a disk it can read.
The cookbook ships with an offline test suite that asserts exactly this. The
tutorial itself is real and runs end to end — the test simply swaps the live
InstaVM SDK for an in-memory stub so it can verify, without spinning up a VM,
that the worker is only ever handed placeholder credentials. Clone it, run
pytest, and build from there.