One read of Claude Developer Platformapi-20261002T143715Z
19 pages moved out of 753 read.
What this read moved
1-19 of 19api/claude-platform-on-aws-iam-actions Changed · +122 / -116 lines
build-with-claude/overview Changed · +14 / -14 lines
manage-claude/workspaces Changed · +4 / -4 lines
managed-agents/memory Changed · +5 / -5 lines
managed-agents/reference Changed · +1 / -13 lines
managed-agents/self-hosted-sandboxes-custom-tools New page · 640 lines, new page
## Serve a custom tool ## Wrap an MCP server as custom tools ### Install an MCP SDK ### Declare and serve the tools ## Limits and behavior ### Tools are declared, not discovered at runtime ### Declarations must fit the Managed Agents API ### Tool failures surface as error tool results ### Wrap only servers you operate or trust ### Permission policies do not apply
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Custom tools in self-hosted sandboxes
url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-custom-tools
description: Serve custom tools from a self-hosted sandbox worker, and wrap an MCP server inside your network as custom tools without running a tunnel.
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
[Custom tools](https://platform.claude.com/docs/en/managed-agents/tools#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`. Your worker can be that code. 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.
<Note>
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 the SDK worker inside the sandbox](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#run-the-sdk-worker-inside-the-sandbox).
</Note>
## Serve a custom tool
<Steps>
<Step title="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](https://platform.claude.com/docs/en/managed-agents/tools#custom-tools) for the full declaration shape.
```json
{
"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"]
}
}
```
</Step>
<Step title="Register the implementation with the worker">
Pass the tool through the worker's `tools` (go: `ToolsFunc`) factory (see [`EnvironmentWorker`](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference#environment-worker)), alongside the built-in toolset:
<CodeGroup exclude="shell">
```python Python
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())
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
import { betaTool } from "@anthropic-ai/sdk/helpers/beta/json-schema";
import { betaAgentToolset20260401 } from "@anthropic-ai/sdk/tools/agent-toolset/node";
const getOrderStatus = betaTool({
name: "get_order_status",
description: "Look up an order in the internal fulfillment system by order ID.",
inputSchema: {
type: "object",
properties: { order_id: { type: "string", description: "The order ID" } },
required: ["order_id"]
},
// Runs on the worker host: call anything the sandbox can reach.
run: async ({ order_id }) => `Order ${order_id}: shipped`
});
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
const client = new Anthropic({ authToken: environmentKey });
const controller = new AbortController();
process.once("SIGTERM", () => controller.abort());
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: controller.signal,
tools: (ctx) => [...betaAgentToolset20260401(ctx), getOrderStatus]
}).run();
```
```csharp C#
// EnvironmentWorker is not currently available in the C# SDK.
// To answer custom tool calls directly, see the session event stream.
```
```go Go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/lib/environments"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/anthropics/anthropic-sdk-go/toolrunner"
"github.com/anthropics/anthropic-sdk-go/tools/agenttoolset"
)
type orderStatusInput struct {
OrderID string `json:"order_id"`
}
func main() {
environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
getOrderStatus := toolrunner.NewBetaTool(
"get_order_status",
"Look up an order in the internal fulfillment system by order ID.",
anthropic.BetaToolInputSchemaParam{
Properties: map[string]any{
"order_id": map[string]any{"type": "string", "description": "The order ID"},
},
Required: []string{"order_id"},
},
// Runs on the worker host: call anything the sandbox can reach.
func(ctx context.Context, input orderStatusInput) (anthropic.BetaToolResultBlockParamContentUnion, error) {
return anthropic.BetaToolResultBlockParamContentUnion{
OfText: &anthropic.BetaTextBlockParam{Text: "Order " + input.OrderID + ": shipped"},
}, nil
},
)
client := anthropic.NewClient(option.WithAuthToken(environmentKey))
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool {
return append(agenttoolset.BetaAgentToolset20260401(env), getOrderStatus)
},
})
if err := worker.Run(ctx); err != nil {
log.Fatalf("worker: %v", err)
}
}
```
```java Java
// EnvironmentWorker is not currently available in the Java SDK.
// To answer custom tool calls directly, see the session event stream.
```
```php PHP
// EnvironmentWorker is not currently available in the PHP SDK.
// To answer custom tool calls directly, see the session event stream.
```
```ruby Ruby
# EnvironmentWorker is not currently available in the Ruby SDK.
# To answer custom tool calls directly, see the session event stream.
```
</CodeGroup>
</Step>
</Steps>
The worker answers only the tools registered with it. If a tool is declared on the agent but no worker or client serves it, the session pauses with a `requires_action` stop reason. It stays paused until something posts the result. See [Handling custom tool calls](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#handling-custom-tool-calls) for the event flow.
## Wrap an MCP server as custom tools
The [MCP connector](https://platform.claude.com/docs/en/managed-agents/mcp-connector) connects to MCP servers from Anthropic's side. A server must therefore expose an HTTP endpoint that Anthropic can reach, directly or through an [MCP tunnel](https://platform.claude.com/docs/en/agents-and-tools/mcp-tunnels/overview).
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:
1. The agent emits an `agent.custom_tool_use` event.
2. The worker, inside your sandbox, forwards the call over its open MCP session to the server on your network.
3. The worker posts the server's response as the `user.custom_tool_result`.
### Install an MCP SDK
The SDK's [Client-side MCP helpers](https://platform.claude.com/docs/en/agents-and-tools/mcp-connector#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"` (python; typescript: `npm install @modelcontextprotocol/sdk`; go: `go get github.com/modelcontextprotocol/go-sdk`).
The examples connect without authentication. To send credentials, configure the `http_client` (typescript: `requestInit`; go: `HTTPClient`) you hand to the MCP transport.
### Declare and serve the tools
<Steps>
<Step title="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.
<CodeGroup exclude="shell">
```python Python
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-5-5",
tools=[
{"type": "agent_toolset_20260401"},
*[to_custom_tool(tool) for tool in listed.tools],
],
)
print(agent.id)
asyncio.run(main())
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const MCP_SERVER_URL = "http://mcp.internal.example.com:8000/mcp";
// Run this wherever you create agents, not on the worker host: it
// authenticates with your Claude API key (ANTHROPIC_API_KEY).
const client = new Anthropic();
const mcpClient = new Client({ name: "declare-agent-tools", version: "1.0.0" });
await mcpClient.connect(new StreamableHTTPClientTransport(new URL(MCP_SERVER_URL)));
const { tools } = await mcpClient.listTools();
const agent = await client.beta.agents.create({
name: "Internal tools agent",
model: "claude-opus-5-5",
tools: [
{ type: "agent_toolset_20260401" },
// The MCP fields map one to one onto a custom tool declaration.
...tools.map((tool) => ({
type: "custom" as const,
name: tool.name,
description: tool.description || tool.name,
input_schema: tool.inputSchema
}))
]
});
console.log(agent.id);
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted-sandboxes-memory New page · 159 lines, new page
## Requirements ## Prepare the host ### Isolate sessions that share a store ## How the worker handles memory ## Configure sync ### Sync interval ### Deletions ## Read-only stores and conflicts ## Troubleshooting
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Memory stores in self-hosted sandboxes
url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory
description: "Attach memory stores to Claude Managed Agents sessions that run in self-hosted sandboxes: prepare the host, configure sync, and handle read-only stores and conflicts."
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
Sessions on a self-hosted environment attach [memory stores](https://platform.claude.com/docs/en/managed-agents/memory) exactly as sessions on cloud environments do. List them in `resources` when you create the session, as shown in [Attach a memory store to a session](https://platform.claude.com/docs/en/managed-agents/memory#attach-a-memory-store-to-a-session). A session accepts up to 8 memory stores.
The difference is who materializes the store. On a self-hosted environment your worker, rather than Anthropic's infrastructure, downloads each store into the sandbox and syncs the agent's changes back.
## Requirements
* **A worker that mounts memory stores:** Use `ant` CLI 1.33.0 or later, or `EnvironmentWorker` from the Python, TypeScript, or Go SDK.
* **A POSIX filesystem:** Windows hosts are not supported, because the worker requires `O_NOFOLLOW` when it opens memory files. A case-sensitive filesystem is recommended, so that memory paths that differ only in case do not collide.
* **A writable `/mnt/memory` directory:** See [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory#prepare-the-host).
* **The work item's secret:** If your own code launches the worker, [forward the work item's secret](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#forward-the-work-items-secret) to it.
<Note>
Memory stores cannot be attached to sessions on self-hosted environments on [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws).
</Note>
## Prepare the host
Before you start the worker, create the parent directory and make it writable by the user the worker runs as:
```bash
sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memory
```
Do not create the per-store directories yourself. The worker creates each store's `mount_path` directory (for example, `/mnt/memory/user-preferences`) when a session starts and removes it when the session ends. If something already exists at that path, the worker refuses to start the session's work.
In the [sandbox-per-session pattern](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#run-one-sandbox-per-session), the sandbox image needs a writable `/mnt/memory`. You don't need to bind-mount the memory directories to the host, because the worker uploads their contents to the store before the sandbox exits.
### Isolate sessions that share a store
Two sessions cannot mount the same store on one host at the same time, because both need the same path. If your sessions attach the same store, run one session per filesystem. Giving each session [its own sandbox](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#run-one-sandbox-per-session) satisfies this rule.
## How the worker handles memory
When the worker claims a work item whose session has memory stores attached, it:
1. **Downloads each store to its `mount_path`.** This is the same directory under `/mnt/memory/` that cloud sessions use, and the session's system prompt describes it to the agent. For example, a store named "User Preferences" lands at `/mnt/memory/user-preferences/`.
2. **Opens those directories to the file tools.** The agent works on memories with the same file tools it uses in the working directory.
3. **Reconciles changes after tool calls,** at most once per sync interval (15 seconds by default). Memories that changed in the store are written to disk, and files the agent changed are uploaded to the store.
4. **Runs a final sync when the session ends.** It flushes any uploads still pending for up to 30 seconds, then removes the directories it created.
The memory store on Anthropic's side remains the source of truth. [Memory versions](https://platform.claude.com/docs/en/managed-agents/memory#audit-memory-changes), redaction, and viewing or editing memories in the Console work as they do for cloud sessions. The agent's memory reads and writes appear in the [event stream](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) as ordinary tool events.
Because each worker syncs on an interval, a change written in one session becomes visible to another running session only after both have synced. That is typically well under a minute at the default interval. Sessions on cloud sandboxes see each other's changes almost immediately.
Each store directory contains a marker file named `.anthropic-memory-store` that ties the directory to its store. Leave it in place: the worker does not sync a directory whose marker is missing or altered.
<Warning>
A worker that is killed rather than stopped runs no teardown, so unsynced edits are lost and the store directories stay behind. See [Stop workers gracefully](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-operations#stop-workers-gracefully).
</Warning>
## Configure sync
Two `EnvironmentWorker` options control memory behavior. Set them wherever you construct the worker, including in a webhook handler. The `ant` CLI worker always uses the defaults.
### Sync interval
`memory_sync_interval` (typescript: `memorySyncIntervalMs`; go: `MemorySyncInterval`) sets how often attached stores reconcile with the server while the session runs.
| Setting | Value |
| ---------------------- | ----------------------------------------------------------- |
| Default | 15 seconds |
| Minimum | 5 seconds |
| Example (10 seconds) | `10` (python; typescript: `10_000`; go: `10 * time.Second`) |
| Disable memory support | `None` (python; typescript: `null`; go: `-1`) |
A shorter interval narrows the window in which another session sees stale memories, at the cost of more memory store requests.
Disable memory support only on workers whose sessions attach no memory stores. A disabled worker neither downloads nor syncs stores, so a session with stores attached runs without them even though its system prompt still describes them.
While memory support is enabled, a work item that arrives without a `secret` for a session with attached stores fails rather than running without memory. See [Memory stores fail to mount](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-operations#memory-stores-fail-to-mount).
### Deletions
`memory_sync_deletions` (typescript: `memorySyncDeletions`; go: `MemorySyncDeletions`) sets whether a file the agent deletes locally is also deleted from the store. Uploads and downloads are unaffected.
| Value | Behavior |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"enabled"` (go: `environments.MemorySyncDeletionsEnabled`) (default) | Deletes the memory from the store once a later sync confirms the file is still gone. |
| `"log_only"` (go: `environments.MemorySyncDeletionsLogOnly`) | Runs the same checks but only logs what it would have deleted. Use it to watch what your workers would delete before you trust the enabled mode. |
| `"disabled"` (go: `environments.MemorySyncDeletionsDisabled`) | Never deletes from the store. |
For example, to sync every 10 seconds and only log the deletes the worker would have made:
<CodeGroup exclude="shell">
```python Python
worker = EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
memory_sync_interval=10, # seconds
memory_sync_deletions="log_only",
)
```
```typescript TypeScript
const worker = new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
memorySyncIntervalMs: 10_000,
memorySyncDeletions: "log_only"
});
```
```csharp C#
// EnvironmentWorker is not currently available in the C# SDK.
```
```go Go
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
MemorySyncInterval: 10 * time.Second,
MemorySyncDeletions: environments.MemorySyncDeletionsLogOnly,
})
```
```java Java
// EnvironmentWorker is not currently available in the Java SDK.
```
```php PHP
// EnvironmentWorker is not currently available in the PHP SDK.
```
```ruby Ruby
# EnvironmentWorker is not currently available in the Ruby SDK.
```
</CodeGroup>
## Read-only stores and conflicts
For a store attached with `access: "read_only"`, the `write` and `edit` tools refuse to change files inside its directory. The worker never uploads anything from it.
Changes made through `bash`, or through a custom tool or MCP server you serve from the sandbox, are not blocked locally. They are never synced to the store, and the next remote change to that memory overwrites them. If the local copy itself must stay unchanged during the session:
* Disable the `bash` tool for that agent, and give it no custom tool that writes to the sandbox's filesystem.
* Do not mount the store path read-only. The worker itself must create the directory and write the downloaded memories into it.
Conflicts resolve in favor of the store. Suppose the agent changes a memory file that also changed in the store since the session last synced it. At the next sync, the worker keeps the store's version, overwrites the local file with it, and logs a warning. The `write` and `edit` tools themselves succeed and no error reaches the agent. If the agent's change still applies, it can re-read the file after the sync and make the change again.
## Troubleshooting
See [Memory stores fail to mount](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-operations#memory-stores-fail-to-mount) for the worker's log messages and their fixes.
managed-agents/self-hosted-sandboxes-operations New page · 353 lines, new page
## Read queue depth ## Stop a session gracefully ## Stop workers gracefully ## Troubleshooting ### The worker doesn't connect ### A session stays queued ### Memory stores fail to mount ### A custom tool call never returns ### A wrapped MCP tool call hangs
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Monitor and troubleshoot self-hosted workers
url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-operations
description: Read queue depth, stop sessions and workers without losing work, and fix common self-hosted sandbox failures.
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
The monitoring calls on this page run from your monitoring or operations tooling, authenticated with your Claude API key. The worker helpers handle the claim and keep-alive loop, so you don't call those endpoints directly.
<Warning>
These endpoints accept either your organization API key or the environment key. Call them from outside the worker host with your organization API key. Setting `ANTHROPIC_API_KEY` on the worker host exposes an organization-scoped credential to agent tool calls.
</Warning>
## Read queue depth
`GET /v1/environments/{environment_id}/work/stats` (curl; python, typescript, ruby: `client.beta.environments.work.stats()`; go, csharp: `client.Beta.Environments.Work.Stats()`; java: `client.beta().environments().work().stats()`; php: `$client->beta->environments->work->stats()`; cli: `ant beta:environments:work stats`) returns the queue state for an environment:
| Field | Meaning | Use it to |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `depth` | Items waiting to be claimed. | Scale your worker fleet or alert on backlog. |
| `pending` | Items claimed by a worker but not yet acknowledged. The worker helpers acknowledge each item before processing it, so this stays near zero in normal operation. | Detect a worker that stalled between claiming and acknowledging: alert on a sustained non-zero value. |
| `oldest_queued_at` | Timestamp of the oldest item still in the queue, either waiting to be claimed or claimed but not yet acknowledged. `null` when there is none. | See how long the oldest item has waited. |
| `workers_polling` | Workers that have polled in the last 30 seconds. | Alert on liveness. |
<CodeGroup>
```bash cURL
curl -sS "https://api.anthropic.com/v1/environments/$ANTHROPIC_ENVIRONMENT_ID/work/stats" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "anthropic-version: 2023-06-01"
```
```bash CLI
ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID"
```
```python Python
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}")
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stats = await client.beta.environments.work.stats(process.env.ANTHROPIC_ENVIRONMENT_ID!);
console.log(`depth=${stats.depth} pending=${stats.pending}`);
```
```csharp C#
using Anthropic;
var client = new AnthropicClient();
var environmentId = Environment.GetEnvironmentVariable("ANTHROPIC_ENVIRONMENT_ID")!;
var stats = await client.Beta.Environments.Work.Stats(environmentId);
Console.WriteLine($"depth={stats.Depth} pending={stats.Pending}");
```
```go Go
package main
import (
"context"
"fmt"
"os"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
client := anthropic.NewClient()
environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")
stats, err := client.Beta.Environments.Work.Stats(
context.Background(),
environmentID,
anthropic.BetaEnvironmentWorkStatsParams{},
)
if err != nil {
panic(err)
}
fmt.Printf("depth=%d pending=%d\n", stats.Depth, stats.Pending)
}
```
```java Java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.beta.environments.work.BetaSelfHostedWorkQueueStats;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
BetaSelfHostedWorkQueueStats stats = client.beta()
.environments()
.work()
.stats(System.getenv("ANTHROPIC_ENVIRONMENT_ID"));
IO.println("depth=" + stats.depth() + " pending=" + stats.pending());
}
```
```php PHP
<?php
use Anthropic\Client;
$client = new Client();
$stats = $client->beta->environments->work->stats(getenv('ANTHROPIC_ENVIRONMENT_ID'));
printf("depth=%d pending=%d\n", $stats->depth, $stats->pending);
```
```ruby Ruby
require "anthropic"
client = Anthropic::Client.new
stats = client.beta.environments.work.stats(ENV.fetch("ANTHROPIC_ENVIRONMENT_ID"))
puts "depth=#{stats.depth} pending=#{stats.pending}"
```
</CodeGroup>
```text wrap
{
"type": "work_queue_stats",
"depth": 0,
"pending": 0,
"oldest_queued_at": null,
"workers_polling": 0
}
```
## Stop a session gracefully
Use `POST /v1/environments/{environment_id}/work/{work_id}/stop` (curl; python, typescript, ruby: `client.beta.environments.work.stop()`; go, csharp: `client.Beta.Environments.Work.Stop()`; java: `client.beta().environments().work().stop()`; php: `$client->beta->environments->work->stop()`; cli: `ant beta:environments:work stop`) to ask the worker handling a specific session to shut it down.
By default the work item moves to `stopping`. The worker notices on its next lease heartbeat, cancels the session's in-flight tool call, and confirms the shutdown. The work item then becomes `stopped`.
Pass `force: true` (python: `force=True`; cli: `--force`) to mark the work item `stopped` immediately instead of waiting for the worker's confirmation.
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](https://platform.claude.com/docs/en/api/beta/environments/work).
<CodeGroup>
```bash cURL
curl -sS "https://api.anthropic.com/v1/environments/$ANTHROPIC_ENVIRONMENT_ID/work/$ANTHROPIC_WORK_ID/stop" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{}'
```
```bash CLI
ant beta:environments:work stop \
--environment-id "$ANTHROPIC_ENVIRONMENT_ID" \
--work-id "$ANTHROPIC_WORK_ID"
```
```python Python
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)
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const work = await client.beta.environments.work.stop(process.env.ANTHROPIC_WORK_ID!, {
environment_id: process.env.ANTHROPIC_ENVIRONMENT_ID!
});
console.log(work.state);
```
```csharp C#
using Anthropic;
var client = new AnthropicClient();
var work = await client.Beta.Environments.Work.Stop(
Environment.GetEnvironmentVariable("ANTHROPIC_WORK_ID")!,
new()
{
EnvironmentID = Environment.GetEnvironmentVariable("ANTHROPIC_ENVIRONMENT_ID")!
}
);
Console.WriteLine(work.State);
```
```go Go
package main
import (
"context"
"fmt"
"os"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
client := anthropic.NewClient()
work, err := client.Beta.Environments.Work.Stop(
context.Background(),
os.Getenv("ANTHROPIC_WORK_ID"),
anthropic.BetaEnvironmentWorkStopParams{
EnvironmentID: os.Getenv("ANTHROPIC_ENVIRONMENT_ID"),
},
)
if err != nil {
panic(err)
}
fmt.Println(work.State)
}
```
```java Java
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.beta.environments.work.BetaSelfHostedWork;
import com.anthropic.models.beta.environments.work.BetaSelfHostedWorkStopRequest;
import com.anthropic.models.beta.environments.work.WorkStopParams;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
BetaSelfHostedWork work = client.beta().environments().work().stop(
WorkStopParams.builder()
.environmentId(System.getenv("ANTHROPIC_ENVIRONMENT_ID"))
.workId(System.getenv("ANTHROPIC_WORK_ID"))
.betaSelfHostedWorkStopRequest(BetaSelfHostedWorkStopRequest.builder().build())
.build()
);
IO.println(work.state());
}
```
```php PHP
<?php
use Anthropic\Client;
$client = new Client();
$work = $client->beta->environments->work->stop(
getenv('ANTHROPIC_WORK_ID'),
environmentID: getenv('ANTHROPIC_ENVIRONMENT_ID'),
);
echo $work->state . "\n";
```
```ruby Ruby
require "anthropic"
client = Anthropic::Client.new
work = client.beta.environments.work.stop(
ENV.fetch("ANTHROPIC_WORK_ID"),
environment_id: ENV.fetch("ANTHROPIC_ENVIRONMENT_ID")
)
puts work.state
```
</CodeGroup>
## Stop workers gracefully
Cut at 300 lines. The page has the rest.
managed-agents/self-hosted-sandboxes-reference New page · 214 lines, new page
## CLI commands and flags ## Environment variables ## Host requirements ## Sandbox filesystem ## SDK helpers ### EnvironmentWorker ### Work poller ### Session tool runner ### AgentToolContext and the agent toolset
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Self-hosted worker reference
url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference
description: "Reference for self-hosted sandbox workers: ant CLI flags, environment variables, host requirements, filesystem paths, and SDK helper options."
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
This page documents the pre-built workers that serve a `self_hosted` environment. For task-oriented guides, start with [Self-hosted sandboxes](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes) and [Deploy self-hosted workers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers).
## CLI commands and flags
| Command | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ant beta:worker poll` | Claims work items from the environment's queue and runs each session in process. With `--on-work`, calls your script for each work item instead. |
| `ant beta:worker run` | Handles one claimed session and exits. Use it as the entrypoint of a per-session sandbox. |
| Flag | Description |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--environment-id` | The environment to poll for work. Also reads from `ANTHROPIC_ENVIRONMENT_ID`. |
| `--environment-key` | Authenticates the worker with this environment. Also reads from `ANTHROPIC_ENVIRONMENT_KEY`. |
| `--workdir` | Directory where skills are downloaded and tools read and write files. Defaults to `.` (the current directory). |
| `--on-work` | Script to call for each claimed work item instead of running tools in-process. Receives session details as environment variables and the work item as JSON on standard input. |
| `--max-idle` | How long to wait after the session goes idle with an `end_turn` [stop reason](https://platform.claude.com/docs/en/build-with-claude/handling-stop-reasons) before shutting down. Defaults to `60s`. |
| `--log-format` | Log output format. Use `json` for structured log ingestion. Defaults to `text`. |
## Environment variables
| Variable | Description | Set by |
| ------------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ANTHROPIC_ENVIRONMENT_ID` | The environment whose queue the worker serves. | You, on the worker host. The poller passes it to the `--on-work` script. |
| `ANTHROPIC_ENVIRONMENT_KEY` | Authenticates the worker to its queue. | You, on the worker host. The poller passes it to the `--on-work` script. |
| `ANTHROPIC_SESSION_ID` | The session that a claimed work item represents. | The poller, for the `--on-work` script. |
| `ANTHROPIC_WORK_ID` | The claimed work item. | The poller, for the `--on-work` script. |
| `ANTHROPIC_WORK_SECRET` | The work item's per-session secret. | You. The poller does not set it. See [Forward the work item's secret](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#forward-the-work-items-secret). |
| `ANTHROPIC_BASE_URL` | Overrides the default API endpoint. Optional. | You, on the worker host. |
| `ANTHROPIC_WEBHOOK_SIGNING_KEY` | Verifies incoming webhook payloads. | You, on a webhook handler host. |
## Host requirements
| Worker | Requirement |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| All workers | A Linux host with `/bin/bash` at that exact path. The worker's bash tool invokes it directly, without consulting `PATH`. |
| TypeScript SDK | `unzip` and `tar` on the `PATH`, and Node.js 22 or later. |
| Python and Go SDKs | No additional binaries. These SDKs use their standard libraries for archive extraction. |
[Memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory#requirements) add their own requirements.
## Sandbox filesystem
| Path | Contents |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/workspace` | The system default working directory for tool execution and skill download. If you use a different working directory, update your agent's system prompt so Claude can locate the skill files. |
| `<workdir>/skills/<name>/` | The agent's downloaded skills. |
| `/mnt/memory/<store>/` | One directory per attached memory store, at the store's `mount_path` (for example, `/mnt/memory/user-preferences/`). The worker creates these directories when it claims the session and removes them when the session ends. |
On self-hosted environments the session's system prompt omits the `/mnt/session/outputs` instruction used on Anthropic-managed sandboxes. Final deliverables land wherever the agent writes them in your sandbox filesystem, typically under the working directory.
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.
## SDK helpers
The Python, TypeScript, and Go SDKs provide three helpers at different levels of control:
| Helper | What it does | Use it when |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| [`EnvironmentWorker`](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference#environment-worker) | Handles polling, setup, and execution end to end. | Most cases. |
| [`work.poller()` (go: `environments.NewWorkPoller()`)](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference#work-poller) | Polls the work queue and gives you each claimed session. | You determine what happens for each session, for example launching a sandbox rather than running tools in-process. |
| [`client.beta.sessions.events.tool_runner()` (typescript: `client.beta.sessions.events.toolRunner()`; go: `client.Beta.Sessions.Events.NewToolRunner()`)](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference#session-tool-runner) | Runs tool calls for a single session, given the session ID and a tool list. | You've already claimed the work and only need the execution layer. |
### EnvironmentWorker
| Method | Description |
| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `run()` (go: `Run()`) | Runs indefinitely, picking up sessions as they arrive. |
| `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`) | Handles a single claimed work item and returns. Pass the work, session, and environment identifiers and the `work_secret` (typescript: `workSecret`; go: `WorkSecret`) explicitly, or let it read the [`ANTHROPIC_*` variables](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference#environment-variables). |
| Option | Description |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `tools` (go: `ToolsFunc`) | A factory that receives the session's `AgentToolContext` and returns the tool list. Defaults to the standard agent toolset. |
| `memory_sync_interval` (typescript: `memorySyncIntervalMs`; go: `MemorySyncInterval`) | How often attached memory stores reconcile with the server while the session runs. See [Sync interval](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory#sync-interval). |
| `memory_sync_deletions` (typescript: `memorySyncDeletions`; go: `MemorySyncDeletions`) | Whether files the agent deletes locally are also deleted from the store. See [Deletions](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory#deletions). |
`EnvironmentWorker` manages the `AgentToolContext` and the toolset automatically. Pass a `tools` (go: `ToolsFunc`) factory to customize the tool list:
<CodeGroup exclude="shell">
```python Python
EnvironmentWorker(client, ..., tools=lambda env: [beta_bash_tool(env), my_custom_tool])
```
```typescript TypeScript
new EnvironmentWorker({
client,
environmentId,
environmentKey,
tools: (ctx) => [betaBashTool(ctx), myCustomTool]
});
```
```csharp C#
// EnvironmentWorker is not currently available in the C# SDK.
// To answer custom tool calls directly, see the session event stream.
```
```go Go
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
ToolsFunc: func(env *agenttoolset.AgentToolContext) []anthropic.BetaTool {
return []anthropic.BetaTool{agenttoolset.BetaBashTool(env), myCustomTool}
},
})
```
```java Java
// EnvironmentWorker is not currently available in the Java SDK.
// To answer custom tool calls directly, see the session event stream.
```
```php PHP
// EnvironmentWorker is not currently available in the PHP SDK.
// To answer custom tool calls directly, see the session event stream.
```
```ruby Ruby
# EnvironmentWorker is not currently available in the Ruby SDK.
# To answer custom tool calls directly, see the session event stream.
```
</CodeGroup>
### Work poller
| Option | Description |
| ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `drain` (go: `Drain`) | Whether to stop polling once the queue is empty rather than waiting for new work. |
| `block_ms` (python; typescript: `blockMs`; go: `BlockMs`) | How long each poll waits for work to arrive before returning, in milliseconds. Must be between 1 and 999; the helper re-polls automatically. Pass `null` (typescript; python: `None`; go: `param.Null[int64]()`) for a non-blocking check. Defaults to a 999 ms long-poll. |
| `reclaim_older_than_ms` (typescript: `reclaimOlderThanMs`; go: `ReclaimOlderThanMs`) | Re-claims work items that were claimed but never acknowledged within this many milliseconds. |
| `auto_stop` (typescript: `autoStop`; go: `AutoStop`) | Whether to post a stop signal for each work item once your loop body finishes with it. Set it to `false` (python: `False`; go: `param.NewOpt(false)`) when whatever runs the work item posts the stop itself. `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`) does, and so does a sandbox you launch that owns the stop call. |
For a complete example, see [Launch sandboxes from the SDK poller](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers#launch-sandboxes-from-the-sdk-poller).
### Session tool runner
`client.beta.sessions.events.tool_runner()` (typescript: `client.beta.sessions.events.toolRunner()`; go: `client.Beta.Sessions.Events.NewToolRunner()`) takes a tool list as `tools` (go: `Tools`). To build that list, set up `AgentToolContext` yourself and call `beta_agent_toolset_20260401(env)` (typescript: `betaAgentToolset20260401(ctx)`; go: `agenttoolset.BetaAgentToolset20260401(env)`):
<CodeGroup exclude="shell">
```python Python
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)
```
```typescript TypeScript
import {
setupSkills,
betaAgentToolset20260401
} from "@anthropic-ai/sdk/tools/agent-toolset/node";
const ctx = { workdir: "/workspace", client, sessionId: work.data.id };
await setupSkills(ctx);
const tools = betaAgentToolset20260401(ctx);
```
```csharp C#
// AgentToolContext is not currently available in the C# SDK.
```
```go Go
env := &agenttoolset.AgentToolContext{Workdir: "/workspace"}
if err := env.SetupSkills(ctx, client, work.Data.ID); err != nil {
panic(err)
}
// skills downloaded to /workspace/skills/<name>/
tools := agenttoolset.BetaAgentToolset20260401(env)
```
```java Java
// AgentToolContext is not currently available in the Java SDK.
```
```php PHP
// AgentToolContext is not currently available in the PHP SDK.
```
```ruby Ruby
# AgentToolContext is not currently available in the Ruby SDK.
```
</CodeGroup>
### AgentToolContext and the agent toolset
`AgentToolContext` is the execution context for tool calls. It defines the working directory and path policy, and can download the session's skills.
| Option | Description |
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `allowed_roots` (typescript: `allowedRoots`; go: `AllowedRoots`) | Directories, in addition to the working directory, that the file tools (`read`, `write`, `edit`, `glob`, `grep`) can reach. |
| `read_only_roots` (typescript: `readOnlyRoots`; go: `ReadOnlyRoots`) | Directories under which `write` and `edit` refuse paths. |
`EnvironmentWorker` adds the session's memory store directories to `allowed_roots` (typescript: `allowedRoots`; go: `AllowedRoots`) itself, and the directories of stores attached with `access: "read_only"` to `read_only_roots` (typescript: `readOnlyRoots`; go: `ReadOnlyRoots`).
The confinement is a guardrail for the file tools only, not a sandbox. It does not constrain `bash`.
`beta_agent_toolset_20260401(env)` (typescript: `betaAgentToolset20260401(ctx)`; go: `agenttoolset.BetaAgentToolset20260401(env)`) takes an `AgentToolContext` and returns the standard tool implementations (`bash`, `read`, `write`, `edit`, `glob`, `grep`).
managed-agents/self-hosted-sandboxes-workers New page · 919 lines, new page
## Choose a deployment pattern ## Run an always-on worker ## Trigger workers from webhooks ## Run one sandbox per session ### Forward the work item's secret ### Launch sandboxes from the SDK poller ### Run the SDK worker inside the sandbox ## Stage files for a session ## Next steps
A whole new page. There's nothing to diff it against, so here is what it says.
---
title: Deploy self-hosted workers
url: https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-workers
description: "Choose how self-hosted sandbox workers claim work and where sessions run: always-on or webhook-triggered, in one process or one sandbox per session."
featureMetadata:
topic:
title: Managed Agents
url: https://platform.claude.com/docs/en/managed-agents/overview
status: beta
betaHeader: managed-agents-2026-04-01
---
The [quickstart](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#quickstart) runs one `ant` CLI worker that polls continuously and runs every session in one process. This page covers the other ways to run a worker and how to choose between them.
## Choose a deployment pattern
When deploying workers, you need to make two choices: how the worker claims work, and where each session runs.
**How the worker claims work:**
* **Always-on:** A long-running process polls the queue continuously and needs only outbound HTTPS. This is the simplest setup.
* **Webhook-triggered:** A handler wakes on `session.status_run_started` and starts polling. This avoids an idle poller, but requires a [webhook](https://platform.claude.com/docs/en/managed-agents/webhooks) endpoint that Anthropic can reach.
**Where each session runs:**
* **In process:** The worker that claims a session also runs its tool calls, in one shared working directory.
* **Sandbox per session:** A poller launches a fresh sandbox for each claimed session. Choose this for stronger isolation: a fresh filesystem, resource limits, or per-session network controls.
The CLI and SDK workers support different combinations:
| Capability | `ant` CLI | SDK (Python, TypeScript, Go) |
| ----------------------------------------------------------------------------------------------------- | ------------------------------- | ---------------------------- |
| Always-on polling | Yes | Yes |
| Webhook-triggered | No | Yes |
| Sandbox per session | Yes | Yes |
| [Memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-memory) | Yes, with default sync settings | Yes, with configurable sync |
| [Custom tools](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-custom-tools) | No | Yes |
See [Self-hosted worker reference](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-reference) for every CLI flag and SDK option. For more control, call the [Environments Work endpoints](https://platform.claude.com/docs/en/api/beta/environments/work) directly and implement your own worker.
## Run an always-on worker
Both workers authenticate with the [environment key](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-your-first-session) from the quickstart.
With the `ant` CLI:
```bash
ant beta:worker poll --workdir /workspace
```
With the SDK, `EnvironmentWorker` does the same work:
<CodeGroup exclude="shell">
```python Python
import asyncio
import contextlib
import os
import signal
from anthropic import AsyncAnthropic
from anthropic.lib.environments import EnvironmentWorker
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:
worker = EnvironmentWorker(
client,
environment_id=environment_id,
environment_key=environment_key,
workdir="/workspace",
)
task = asyncio.create_task(worker.run())
# Cancelling the task, rather than killing the process, lets the worker stop its
# in-flight work item and upload changed memory files before it exits.
loop = asyncio.get_running_loop()
for signum in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(signum, task.cancel)
with contextlib.suppress(asyncio.CancelledError):
await task
asyncio.run(main())
```
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
const client = new Anthropic({ authToken: environmentKey });
const controller = new AbortController();
// Aborting on either signal lets the worker upload changed memory files and remove its
// store directories before the process exits.
process.once("SIGINT", () => controller.abort());
process.once("SIGTERM", () => controller.abort());
await new EnvironmentWorker({
client,
environmentId,
environmentKey,
workdir: "/workspace",
signal: controller.signal
}).run();
```
```csharp C#
// EnvironmentWorker is not currently available in the C# SDK. Use the ant CLI worker instead.
```
```go Go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/lib/environments"
"github.com/anthropics/anthropic-sdk-go/option"
)
func main() {
environmentKey := os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")
environmentID := os.Getenv("ANTHROPIC_ENVIRONMENT_ID")
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
client := anthropic.NewClient(option.WithAuthToken(environmentKey))
worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
EnvironmentID: environmentID,
EnvironmentKey: environmentKey,
Workdir: "/workspace",
})
if err := worker.Run(ctx); err != nil {
log.Fatalf("worker: %v", err)
}
}
```
```java Java
// EnvironmentWorker is not currently available in the Java SDK. Use the ant CLI worker instead.
```
```php PHP
// EnvironmentWorker is not currently available in the PHP SDK. Use the ant CLI worker instead.
```
```ruby Ruby
# EnvironmentWorker is not currently available in the Ruby SDK. Use the ant CLI worker instead.
```
</CodeGroup>
## Trigger workers from webhooks
<Steps>
<Step title="Subscribe to session webhooks">
In the [Console](https://platform.claude.com/settings/workspaces/default/webhooks), define a webhook endpoint that listens for `session.status_run_started` events. See [Webhooks](https://platform.claude.com/docs/en/managed-agents/webhooks) for details.
</Step>
<Step title="Export the webhook signing key">
Along with the environment ID and key from the [quickstart](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-your-first-session), export the webhook signing key on your handler host. The handler uses it to verify incoming payloads.
```bash
export ANTHROPIC_WEBHOOK_SIGNING_KEY="whsec_..."
```
</Step>
<Step title="Implement the webhook handler">
Invoke the worker when `session.status_run_started` fires. The handler drains the queue and hands each claimed work item to `handle_item()` (typescript: `handleItem()`; go: `HandleItem()`), which downloads skills, executes tool calls, posts results back, and returns.
<CodeGroup exclude="shell">
<CodeGroupItem>
To verify webhook signatures, install the webhooks extra: `pip install "anthropic[webhooks]"`.
```python Python
import asyncio
import os
import anthropic
import standardwebhooks # installed by the anthropic[webhooks] extra
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
client = anthropic.AsyncAnthropic(
auth_token=environment_key,
)
# Cancelled by shutdown() so an in-flight work item can upload changed memory files and
# remove its store directories before the process exits.
inflight: set[asyncio.Task[None]] = set()
# Await this from the host's shutdown hook, such as an ASGI lifespan shutdown (the code after
# `yield` in a FastAPI lifespan), which uvicorn runs on SIGTERM. uvicorn lets open requests
# finish before that hook runs, so set --timeout-graceful-shutdown to bound the wait.
async def shutdown() -> None:
for task in inflight:
task.cancel()
await asyncio.gather(*inflight, return_exceptions=True)
async def handle(raw: bytes, headers: dict[str, str]) -> tuple[dict[str, str], int]:
try:
event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
except standardwebhooks.WebhookVerificationError:
return {"error": "signature verification failed"}, 401
if event.data.type != "session.status_run_started":
return {"status": "ignored"}, 200
task = asyncio.create_task(run_queued_work())
inflight.add(task)
task.add_done_callback(inflight.discard)
try:
# Shielded: a dropped or timed-out delivery must not cancel the item; shutdown() does.
await asyncio.shield(task)
except asyncio.CancelledError:
return {"status": "shutting down"}, 503
return {"status": "ok"}, 200
async def run_queued_work() -> None:
async for work in client.beta.environments.work.poller(
environment_id=environment_id,
environment_key=environment_key,
block_ms=None,
reclaim_older_than_ms=2000,
drain=True,
auto_stop=False,
):
await client.beta.environments.work.worker(workdir="/workspace").handle_item(
work_id=work.id,
environment_id=environment_id,
session_id=work.data.id,
environment_key=environment_key,
# The per-session secret is what lets the worker mount the session's memory stores.
work_secret=work.secret,
)
```
</CodeGroupItem>
```typescript TypeScript
import Anthropic from "@anthropic-ai/sdk";
const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
const client = new Anthropic({
authToken: environmentKey
});
// Call shutdown.abort() from the host's SIGTERM/SIGINT handler, alongside closing the server,
// then wait for in-flight handle() calls before exiting: the abort lets a running work item
// upload changed memory files and remove its store directories first.
export const shutdown = new AbortController();
export async function handle(req: Request): Promise<Response> {
// Never acknowledge a delivery whose work will not run here; a 503 makes the sender retry.
if (shutdown.signal.aborted) {
return Response.json({ status: "shutting down" }, { status: 503 });
}
const body = await req.text();
let event;
try {
event = client.beta.webhooks.unwrap(body, { headers: Object.fromEntries(req.headers) });
} catch {
return new Response("signature verification failed", { status: 401 });
}
if (event.data.type !== "session.status_run_started") {
return Response.json({ status: "ignored" });
}
for await (const work of client.beta.environments.work.poller({
environmentId,
environmentKey,
blockMs: null,
reclaimOlderThanMs: 2000,
drain: true,
autoStop: false,
signal: shutdown.signal
})) {
await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({
workId: work.id,
environmentId,
sessionId: work.data.id,
environmentKey,
// The per-session secret is what lets the worker mount the session's memory stores.
workSecret: work.secret ?? undefined,
signal: shutdown.signal
});
}
// The poller and handleItem return quietly on abort, so a drain cut short lands here.
if (shutdown.signal.aborted) {
return Response.json({ status: "shutting down" }, { status: 503 });
}
return Response.json({ status: "ok" });
}
```
Cut at 300 lines. The page has the rest.