We use cookies to deliver and improve our services, analyze site usage, and if you agree, to customize or personalize your experience and market our services to you. You can read our Cookie Policy here.
By default, Managed Agents executes tools and code inside Anthropic-managed cloud sandboxes. Self-hosted sandboxes keep the orchestration on Anthropic's side but move tool execution into infrastructure you control, so the agent's code, filesystem, and network egress never leave your environment.
Tool execution stays on your host: the filesystem the agent reads and writes, the processes it spawns, and the network it can reach are all under your control. Tool inputs and outputs still flow to Anthropic's control plane (where Claude runs) so the model can see results and determine what to do next. See the security model for the full data-flow boundary.
Self-hosted sandboxes support all Claude models available in Managed Agents, including Claude Opus 4.8. The model is configured on the agent, not the environment.
| Cloud environment | Self-hosted sandbox | |
|---|---|---|
| Where tools run | Anthropic-managed sandboxes | Your infrastructure |
| Network reach | Anthropic's egress controls | Your network policy |
| File and GitHub repo mounting | Managed by Anthropic | Managed by you |
| Lifecycle | Managed by Anthropic | Managed by you |
Self-hosting is a good fit when the agent needs to operate on data that cannot leave your network boundary, reach internal services that are not publicly routable, or run under your organization's own compliance and audit controls.
For Zero Data Retention and HIPAA BAA eligibility, see API and data retention.
Self-hosting controls where the agent's code executes. MCP tunnels control how Anthropic reaches MCP servers in your network. They are independent: a session running in Anthropic's cloud sandboxes can still reach private MCP servers through a tunnel, and a self-hosted session can use either tunneled or public MCP servers. Use both when you want execution and tool access to stay inside your boundary. To give the agent tools from an MCP server inside your network without running a tunnel, you can also wrap the server as custom tools served by your worker.
This guide describes how to build a worker with any generic sandboxing platform. Additional, platform-specific guides are available for AWS Lambda MicroVMs, Blaxel, Cloudflare, Daytona, E2B, GKE Agent Sandbox, Modal, Namespace, Superserve, and Vercel.
An environment worker is a process you run on your own infrastructure. It receives tool execution requests from Anthropic and runs them locally. The self_hosted environment acts as a work queue: when a session is assigned to it, Anthropic enqueues the session as a work item. Your worker claims work items from that queue, spawns an execution context for each one, downloads the agent's skills (reusable, filesystem-based resources that give the agent domain-specific expertise), runs the tool calls, and posts the results back.
Work items are claimed by polling the environment's queue: either by an always-on worker that polls continuously, or a webhook-triggered handler that wakes on session.status_run_started and starts polling.
The CLI and SDK both ship pre-built workers. The ant CLI supports the always-on pattern only; the SDK supports both always-on and webhook-triggered. Both are configurable: see Self-hosted worker in the reference for CLI flags, and SDK helpers on this page for the SDK options. For more control, call the Environments Work endpoints directly and implement your own worker.
/workspace: the system default working directory for tool execution and skill download. The CLI's --workdir flag defaults to the current directory; pass --workdir /workspace to match the system default. Skills are downloaded to <workdir>/skills/<name>/. If you use a different working directory, update your agent's system prompt so Claude can locate the skill files./mnt/session/outputs: the worker harness instructs Claude to write final deliverables here. In sandbox mode, mount a host directory at this path to retrieve outputs after the session ends. In in-process mode, the worker's file tools write under the working directory instead, so this path does not apply.You need:
/bin/bash at that exact path. The worker's bash tool invokes it directly, without consulting PATH. The TypeScript SDK additionally requires unzip and tar on the PATH and Node.js 22 or later; the Python and Go SDKs use their standard libraries for archive extraction and have no additional binary requirements.ant CLI or an Anthropic SDK (Python, TypeScript, or Go) on the worker host.On Claude Platform on AWS, the worker authenticates with AWS IAM (SigV4) or an API key generated in the AWS Console, not an environment key. Attach the AnthropicSelfHostedEnvironmentAccess managed policy to the IAM principal your worker runs as. Environment keys generated in the Claude Console don't work with the Claude Platform on AWS endpoint.
Create a self-hosted environment
In the Console: Workspace > Environments > New > Self-hosted
Or through the API:
client = anthropic.Anthropic()
environment = client.beta.environments.create(
name="self-hosted", config={"type": "self_hosted"}
)
print(environment.id)Generate an environment key
In the Console, open the environment and click Generate environment key. Key generation is Console-only, regardless of whether you created the environment through the Console or the API. Then export the environment ID and key on the worker host:
export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."
export ANTHROPIC_ENVIRONMENT_ID="env_..."Skills can include executables that the agent may run directly. The CLI and SDK workers preserve the executable permissions recorded in the skill bundle when they extract it. If you implement skills download manually, you are responsible for setting executable permissions.
Choose always-on for the simplest setup: a long-running process polls the queue continuously and needs only outbound HTTPS. Choose webhook-triggered to avoid running an idle poller; it requires a webhook endpoint that Anthropic can reach (see Webhooks for endpoint setup and signature verification).
The SDK provides three helpers at different levels of control. EnvironmentWorker covers most use cases; drop to the lower-level helpers when you need to launch your own per-session process or run tools against an already-claimed session.
EnvironmentWorker: the out-of-the-box worker. Handles polling, setup, and execution end to end.
.run(): runs indefinitely, picking up sessions as they arrive..handle_item(): handles a single claimed work item and exits. Pass the work, session, and environment identifiers explicitly, or let it read the ANTHROPIC_* variables that ant beta:worker poll --on-work sets for the process it spawns.work.poller(): polls the work queue on your behalf and gives you each claimed session. Use this when you want to decide what happens for each session, for example launching a sandbox rather than running tools in-process.
drain: whether to stop polling once the queue is empty rather than waiting for new work.block_ms: how long to wait for work to arrive before returning, in milliseconds. Must be between 1 and 999 (per-poll wait; the helper re-polls automatically). Pass null (None in Python, param.Null[int64]() in Go) for a non-blocking check; omitting the parameter uses the default 999 ms long-poll.reclaim_older_than_ms: re-claim work items that were claimed but never acknowledged within this many milliseconds.auto_stop: whether to post a stop signal for each work item once your loop body finishes with it. The Go poller has no opt-out and always posts the stop signal, so block in the loop body until the session completes rather than detaching.client.beta.sessions.events.tool_runner(): runs tool calls for a single session, given the session ID and a tool list. Use when you've already claimed the work and only need the execution layer.Use the work poller directly when you want to launch your own per-session process, for example spinning up a sandbox for each claimed session:
import asyncio
import os
from anthropic import AsyncAnthropic
from anthropic.types.beta.environments import BetaSelfHostedWork
async def launch_container(work: BetaSelfHostedWork) -> None:
# Replace with your own per-session sandbox launcher. Pass
# ANTHROPIC_ENVIRONMENT_KEY into the launched sandbox, never
# your API key.
print(f"claimed session {work.data.id}")
async def main() -> None:
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
async with AsyncAnthropic(auth_token=environment_key) as client:
async for work in client.beta.environments.work.poller(
environment_id=environment_id,
environment_key=environment_key,
auto_stop=False, # the launched sandbox owns the stop call
):
await launch_container(work)
asyncio.run(main())AgentToolContext is the execution context for tool calls. It defines the working directory and path policy, and can download the session's skills. beta_agent_toolset_20260401(env) takes an AgentToolContext and returns the standard tool implementations (bash, read, write, edit, glob, grep).
With EnvironmentWorker: both are managed automatically. Pass a tools factory to customize the tool list:
EnvironmentWorker(client, ..., tools=lambda env: [beta_bash_tool(env), my_custom_tool])With work.poller() and tool_runner(): pass a tool list as tools to client.beta.sessions.events.tool_runner(). To build that list, set up AgentToolContext yourself and call beta_agent_toolset_20260401(env):
from anthropic.lib.tools.agent_toolset import (
AgentToolContext,
beta_agent_toolset_20260401,
)
async with AgentToolContext(
workdir="/workspace", client=client, session_id=work.data.id
) as env:
# skills downloaded to /workspace/skills/<name>/
tools = beta_agent_toolset_20260401(env)From a separate shell, with ANTHROPIC_API_KEY set to your Claude API key (not the environment key), confirm workers_polling is at least 1:
ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID"If workers_polling stays at 0, the worker isn't reaching the queue: confirm ANTHROPIC_ENVIRONMENT_KEY and ANTHROPIC_ENVIRONMENT_ID are set on the worker host. See Read queue depth for the full stats response and other language examples.
Once your worker is running, create a session that targets the environment. Set AGENT_ID to the agent ID you noted in Before you begin. The session enters the environment's work queue and waits there until a worker claims it; if no worker is connected, the session stays queued rather than failing.
Anthropic doesn't mount files or GitHub repositories into self-hosted sandboxes. To make session-specific files available, pass file references (such as an S3 path or commit SHA) in the session metadata field. Your spawn script or --on-work handler reads that metadata from the claimed work item (the CLI poller pipes the work item's JSON to the script's stdin, and SDK handlers can read it through the Environments Work endpoints) and stages the files into the working directory before tool execution begins.
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
metadata={"input_file": "s3://my-bucket/data.csv"},
)Memory is not currently supported with self-hosted sandboxes.
See Self-hosted worker in the reference for the full list of CLI flags, and SDK helpers for the SDK helper options.
Custom tools are tools your own code executes: the agent emits an agent.custom_tool_use event and waits for a matching user.custom_tool_result. The worker can be that code, and because it runs inside your sandbox, the tool reaches the internal services, credentials, and network egress you configured for the sandbox, and nothing more. The environment key authorizes posting custom tool results, so your Claude API key stays off the worker host.
Serving custom tools requires the SDK worker: the ant CLI worker has no way to register a custom tool implementation. In the sandbox-per-session pattern, run EnvironmentWorker inside the sandbox with handle_item() (handleItem in TypeScript, HandleItem in Go) in place of ant beta:worker run.
Declare the tool on the agent
Add a custom entry to the agent's tools whose name matches the tool your worker registers. See Custom tools for the full declaration shape.
{
"type": "custom",
"name": "get_order_status",
"description": "Look up an order in the internal fulfillment system by order ID.",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string", "description": "The order ID" }
},
"required": ["order_id"]
}
}Register the implementation with the worker
Pass the tool through the worker's tools factory (see SDK helpers), alongside the built-in toolset:
import asyncio
import os
from anthropic import AsyncAnthropic, beta_async_tool
from anthropic.lib.environments import EnvironmentWorker
from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401
@beta_async_tool
async def get_order_status(order_id: str) -> str:
"""Look up an order in the internal fulfillment system by order ID."""
# Runs on the worker host: call anything the sandbox can reach.
return f"Order {order_id}: shipped"
async def main() -> None:
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
async with AsyncAnthropic(auth_token=environment_key) as client:
await EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
tools=lambda env: [*beta_agent_toolset_20260401(env), get_order_status],
).run()
asyncio.run(main())The worker answers only the tools registered with it. A custom tool that is declared on the agent but registered with no worker or client leaves the session paused with a requires_action stop reason until something posts its result; see Handling custom tool calls for the event flow.
The MCP connector connects to MCP servers from Anthropic's side, so a server must expose an HTTP endpoint that Anthropic can reach, directly or through an MCP tunnel. To use a server that only your network can reach, make the worker the MCP client instead and declare the server's tools as custom tools. The MCP server needs no inbound connectivity from outside your network; Anthropic receives the tool definitions you declare on the agent, each call's input, and the result your worker posts back. At runtime the model calls a wrapped tool like any other custom tool:
agent.custom_tool_use event.user.custom_tool_result.The SDKs' Client-side MCP helpers convert the server's tools into the runnable tools the worker accepts; install an MCP SDK alongside the Anthropic SDK (pip install "anthropic[mcp]" "mcp>=1.24", npm install @modelcontextprotocol/sdk, go get github.com/modelcontextprotocol/go-sdk). The examples connect without authentication; to send credentials, configure the HTTP client or request options you hand to the MCP transport (http_client in Python, requestInit in TypeScript, HTTPClient in Go).
Declare the server's tools on the agent
List the MCP server's tools and declare each one as a custom tool; the MCP name, description, and inputSchema map one to one onto the custom tool's fields. If the server paginates its tool list, declare every page; the worker must list the same pages.
import asyncio
from typing import Any, cast
from anthropic import AsyncAnthropic
from anthropic.types.beta import BetaManagedAgentsCustomToolParams
from mcp import ClientSession, types
# Requires mcp >= 1.24, which renamed streamablehttp_client to streamable_http_client.
from mcp.client.streamable_http import streamable_http_client
MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"
def to_custom_tool(tool: types.Tool) -> BetaManagedAgentsCustomToolParams:
# The MCP fields map one to one onto a custom tool declaration. The cast
# hands the schema dictionary to the SDK's typed parameter unchanged.
return {
"type": "custom",
"name": tool.name,
"description": tool.description or tool.name,
"input_schema": cast(Any, tool.inputSchema),
}
async def main() -> None:
# Run this wherever you create agents, not on the worker host: it
# authenticates with your Claude API key (ANTHROPIC_API_KEY).
async with (
streamable_http_client(MCP_SERVER_URL) as (read, write, _),
ClientSession(read, write) as mcp_session,
AsyncAnthropic() as client,
):
await mcp_session.initialize()
listed = await mcp_session.list_tools()
agent = await client.beta.agents.create(
name="Internal tools agent",
model="claude-opus-4-8",
tools=[
{"type": "agent_toolset_20260401"},
*[to_custom_tool(tool) for tool in listed.tools],
],
)
print(agent.id)
asyncio.run(main())Serve the tools from the worker
Connect to the same MCP server at startup, convert its tools with the MCP helpers, and register them alongside the built-in toolset. Keep one MCP session open for the life of the worker.
import asyncio
import os
from datetime import timedelta
from anthropic import AsyncAnthropic
from anthropic.lib.environments import EnvironmentWorker
from anthropic.lib.tools.agent_toolset import beta_agent_toolset_20260401
from anthropic.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
# Requires mcp >= 1.24, which renamed streamablehttp_client to streamable_http_client.
from mcp.client.streamable_http import streamable_http_client
MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp"
async def main() -> None:
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
# Connect to the MCP server once at startup and keep the session open for
# the life of the worker. The timeout turns a hung tool call into an error
# result instead of a stalled call.
async with (
streamable_http_client(MCP_SERVER_URL) as (read, write, _),
ClientSession(read, write, read_timeout_seconds=timedelta(seconds=60)) as mcp_session,
AsyncAnthropic(auth_token=environment_key) as client,
):
await mcp_session.initialize()
listed = await mcp_session.list_tools()
mcp_tools = [async_mcp_tool(tool, mcp_session) for tool in listed.tools]
await EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
tools=lambda env: [*beta_agent_toolset_20260401(env), *mcp_tools],
).run()
asyncio.run(main())Keep the following in mind when you wrap an MCP server:
tools array takes at most 128 entries (each wrapped tool is one entry, and the built-in toolset is one more). The API rejects a declaration that reuses a tool name, names a custom tool after a built-in agent tool such as bash or read, or uses the reserved mcp__ prefix. The MCP helpers keep the server's names and descriptions, so rename or trim where needed. When two servers expose the same tool name, define the wrapper yourself under a prefixed name and have it call the server's original tool name.additionalProperties and title. It rejects reference keywords such as $ref anywhere in a custom tool's input_schema, so inline the schemas that generators such as pydantic factor into $defs. It also rejects top-level oneOf, anyOf, and allOf, and property names outside letters, digits, underscores, dots, and hyphens (1–64 characters).read_timeout_seconds. Without one, a hung call becomes an error result only when the TypeScript MCP SDK's default request timeout fires (about a minute) or, in Python, when the worker's own backstop does (about two and a half minutes). In Go neither the MCP client nor the worker applies a default: a hung call waits until the session's context ends, so bound the per-call context with a deadline.bash on the worker host. Declare only the tools you intend the agent to use.These calls run from your monitoring or operations tooling, authenticated with your Claude API key, to observe and manage the worker fleet. The claim and keep-alive loop is handled inside the worker helpers, so you don't call those endpoints directly.
These endpoints authenticate with your organization API key, not the environment key. Call them from outside the worker host. Setting ANTHROPIC_API_KEY on the worker host exposes an organization-scoped credential to agent tool calls.
work.stats returns the queue state for an environment:
depth is the number of items waiting to be claimed. Scale your worker fleet or alert on backlog based on this value.pending is the number of items a worker has claimed and is currently processing.oldest_queued_at is the timestamp of the oldest item still queued or being processed, or null when there is none.workers_polling is the number of workers that have polled in the last 30 seconds. Use this for liveness alerting.import os
import anthropic
client = anthropic.Anthropic()
stats = client.beta.environments.work.stats(os.environ["ANTHROPIC_ENVIRONMENT_ID"])
print(f"depth={stats.depth} pending={stats.pending}"){
"type": "work_queue_stats",
"depth": 0,
"pending": 0,
"oldest_queued_at": null,
"workers_polling": 0
}Use work.stop to ask the worker handling a specific session to shut it down cleanly. The worker finishes any in-flight tool call, posts a final status, and releases the session. Pass force: true in the request body (with the CLI, pass --force) to interrupt immediately instead of waiting for the current tool call to complete.
Because these calls run from your operations tooling rather than the worker host, ANTHROPIC_WORK_ID isn't set automatically. Set it to the target work item's ID before running the following examples. To find a work item's ID, list the environment's work items through the Environments Work endpoints.
import os
import anthropic
client = anthropic.Anthropic()
work = client.beta.environments.work.stop(
os.environ["ANTHROPIC_WORK_ID"],
environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"],
)
print(work.state)Shared responsibility model for self-hosted sandbox environments.
Create a session to run your agent and begin executing tasks.
Securely connect Claude to MCP servers running in your private network without opening inbound ports or exposing services to the public internet.
Was this page helpful?