Follow Discord
Sweep 22 Oct 2026 · 04:32Z Build v2.1.308 527 read Stable v2.1.308 Latest v2.1.308 Next v2.1.308 Feeds RSS JSON llms.txt llms-full.txt Unofficial
One change · api

spawn.sh: called once per claimed work item changedmanaged-agents/self-hosted-sandboxes

Nearest release: v2.1.237, published an hour after this site recorded the change. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.

Recorded here
Lines+440added
Lines−39removed
From line 6 where the diff opens
First seen 14 Aug 2026 this site's first read of the page
Recorded edits17to this page, all time

## Use memory stores ### How the worker handles memory ### Prepare the host ### Run one sandbox per session ### Configure sync ### Read-only stores and conflicts ### Troubleshoot memory mounts

The whole hunk

from line 6, old and new numbered
/
lines

The two sides of this change are more than 400 edits apart, too far apart to line up, so this is the differ's own diff of it and the words inside a line are not marked.

from line 6
66 
77By default, Managed Agents executes tools and code inside [Anthropic-managed cloud sandboxes](https://platform.claude.com/docs/en/managed-agents/cloud-sandboxes-reference). 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.
88 
9Tool 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](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security) for the full data-flow boundary.
9Tool 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. The agent's [skills](https://platform.claude.com/docs/en/managed-agents/skills) and the contents of any [memory stores](https://platform.claude.com/docs/en/managed-agents/memory) attached to the session are stored by Anthropic and copied into your sandbox for the session; changes the agent makes to memory files sync back to the store. See the [security model](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes-security) for the full data-flow boundary.
1010 
1111<Note>
1212 Self-hosted sandboxes support all Claude models available in Managed Agents, including Claude Opus 4.8 and Claude Opus 5. The model is configured on the [agent](https://platform.claude.com/docs/en/managed-agents/agent-setup), not the environment.
from line 14
1414 
1515## How it differs from cloud environments
1616 
17| | Cloud environment | Self-hosted sandbox |
18| ----------------------------- | --------------------------- | ------------------- |
19| Where tools run | Anthropic-managed sandboxes | Your infrastructure |
20| Network reach | Anthropic's egress controls | Your network policy |
21| File and GitHub repo mounting | Managed by Anthropic | Managed by you |
22| Lifecycle | Managed by Anthropic | Managed by you |
17| | Cloud environment | Self-hosted sandbox |
18| ----------------------------- | -------------------------------------- | --------------------------------------------------------- |
19| Where tools run | Anthropic-managed sandboxes | Your infrastructure |
20| Network reach | Anthropic's egress controls | Your network policy |
21| File and GitHub repo mounting | Managed by Anthropic | Managed by you |
22| Memory stores | Mounted by Anthropic at `/mnt/memory/` | Downloaded to `/mnt/memory/` and synced by the SDK worker |
23| Lifecycle | Managed by Anthropic | Managed by you |
2324 
2425Self-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.
2526 
from line 46
4546 
4647* **`/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.
4748* **Outputs:** on self-hosted environments the session's system prompt omits the `/mnt/session/outputs` instruction used on Anthropic-managed sandboxes, so final deliverables land wherever the agent writes them in your sandbox filesystem, typically under the working directory.
49* **`/mnt/memory/`:** memory stores attached to the session are materialized here by the SDK worker, one directory per 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; see [Use memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores).
4850 
4951## Before you begin
5052 
from line 55
5355* **An existing agent.** If you don't have one, complete the [Quickstart](https://platform.claude.com/docs/en/managed-agents/quickstart) first and note its agent ID.
5456* **A Linux host** with `/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.
5557* **The `ant` CLI or an Anthropic SDK** (Python, TypeScript, or Go) on the worker host.
56* **Two credentials:** an environment key (generated in the Console in the steps that follow) authenticates the worker to its queue; your Claude API key creates sessions and reads queue stats from outside the worker host. Key generation is Console-only.
58* **Credentials:** an environment key (generated in the Console in the steps that follow) authenticates the worker to its queue; your Claude API key creates sessions and reads queue stats from outside the worker host. Key generation is Console-only. Claimed work items also carry a per-session `secret` that the worker uses to mount [memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores); you don't generate it, but in the sandbox-per-session pattern you forward it into the sandbox yourself (see [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session)).
59* **For memory stores, a prepared host.** If sessions on this environment will attach memory stores, prepare `/mnt/memory` on the worker host before you start the worker; see [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host).
5760 
5861<Note>
5962 On [Claude Platform on AWS](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws), the worker authenticates with AWS IAM (SigV4) or an [API key generated in the AWS Console](https://platform.claude.com/docs/en/build-with-claude/claude-platform-on-aws#api-key-authentication), not an environment key. Attach the [`AnthropicSelfHostedEnvironmentAccess`](https://platform.claude.com/docs/en/api/claude-platform-on-aws-iam-actions#managed-policies) 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.
63 
64 Memory stores cannot be attached to sessions on self-hosted environments on Claude Platform on AWS.
6065</Note>
6166 
6267<Steps>
from line 255
250255 ENTRYPOINT ["ant", "beta:worker", "run"]
251256 ```
252257 
253 Then write a spawn script that forwards session details into a fresh sandbox. The poller injects `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, `ANTHROPIC_ENVIRONMENT_ID`, and `ANTHROPIC_ENVIRONMENT_KEY` into the script's environment. `ANTHROPIC_BASE_URL` is optional and is passed through only if it was set on the poller host; it overrides the default API endpoint. In the example, `/host/outputs` is a host directory you choose; it is bind-mounted to the sandbox's working directory (`/workspace`) so you can retrieve session deliverables after the sandbox exits. On self-hosted environments the agent writes deliverables under the working directory rather than `/mnt/session/outputs` (see [Sandbox filesystem](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sandbox-filesystem)), so mounting the working directory is what captures them; the mount also picks up the downloaded `skills/` tree and any intermediate files the agent creates.
258 Then write a spawn script that forwards session details into a fresh sandbox. The poller injects `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, `ANTHROPIC_ENVIRONMENT_ID`, and `ANTHROPIC_ENVIRONMENT_KEY` into the script's environment, and writes the claimed work item to the script's standard input as JSON, including the work item's per-session `secret` when Anthropic issued one. `ANTHROPIC_BASE_URL` is optional and is passed through only if it was set on the poller host; it overrides the default API endpoint. In the example, `/host/outputs` is a host directory you choose; it is bind-mounted to the sandbox's working directory (`/workspace`) so you can retrieve session deliverables after the sandbox exits. On self-hosted environments the agent writes deliverables under the working directory rather than `/mnt/session/outputs` (see [Sandbox filesystem](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sandbox-filesystem)), so mounting the working directory is what captures them; the mount also picks up the downloaded `skills/` tree and any intermediate files the agent creates.
254259 
255260 ```bash
256261 #!/bin/bash
from line 268
263268 your-image
264269 ```
265270 
271 The `ant beta:worker run` entrypoint does not mount [memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores). If sessions on this environment attach memory stores, keep the poller, but build the per-session image around the SDK worker and extend the spawn script to forward the work item's `secret` into the sandbox, as shown in [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session).
272 
266273 Start the poller pointing at the script:
267274 
268275 ```bash
from line 288
281288 <CodeGroup exclude="shell">
282289 ```python Python
283290 import asyncio
291 import contextlib
284292 import os
293 import signal
285294 from anthropic import AsyncAnthropic
286295 from anthropic.lib.environments import EnvironmentWorker
287296 
from line 299
290299 environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
291300 environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
292301 async with AsyncAnthropic(auth_token=environment_key) as client:
293 await EnvironmentWorker(
302 worker = EnvironmentWorker(
294303 client,
295304 environment_id=environment_id,
296305 environment_key=environment_key,
297306 workdir="/workspace",
298 ).run()
307 )
308 task = asyncio.create_task(worker.run())
309 # Cancelling the task, rather than killing the process, lets the worker stop its
310 # in-flight work item and upload changed memory files before it exits.
311 loop = asyncio.get_running_loop()
312 for signum in (signal.SIGINT, signal.SIGTERM):
313 loop.add_signal_handler(signum, task.cancel)
314 with contextlib.suppress(asyncio.CancelledError):
315 await task
299316 
300317 
301318 asyncio.run(main())
from line 326
309326 const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
310327 const client = new Anthropic({ authToken: environmentKey });
311328 const controller = new AbortController();
329 // Aborting on either signal lets the worker upload changed memory files and remove its
330 // store directories before the process exits.
331 process.once("SIGINT", () => controller.abort());
312332 process.once("SIGTERM", () => controller.abort());
313333 
314334 await new EnvironmentWorker({
from line 413
393413 <Step title="Implement the webhook handler">
394414 `EnvironmentWorker` claims the work item, downloads skills, executes tool calls in the working directory, posts results back, and exits. Invoke it when `session.status_run_started` fires.
395415 
416 When you hand a claimed work item to `handle_item()` yourself, as this handler does, pass the work item's `secret` along as `work_secret` (`workSecret` in TypeScript, `WorkSecret` in Go) so the session can mount any [memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores) attached to it. A handler like this one runs every claimed item in one process on one host, so two sessions that attach the same memory store cannot run through it at the same time (see [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host)); if your sessions share stores, launch [one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session) instead.
417 
396418 <CodeGroup exclude="shell">
397419 ```python Python
420 import asyncio
398421 import os
422 import signal
399423 import anthropic
424 import standardwebhooks # installed by the anthropic[webhooks] extra
400425 
401426 environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
402427 environment_id = os.environ["ANTHROPIC_ENVIRONMENT_ID"]
403428 client = anthropic.AsyncAnthropic(
404429 auth_token=environment_key,
405430 )
406 
407 
408 async def handle(raw: bytes, headers: dict[str, str]) -> dict:
409 event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
431 # Cancelled on SIGINT or SIGTERM (wired in handle) so an in-flight work item can upload
432 # changed memory files and remove its store directories before the process exits.
433 inflight: set[asyncio.Task[None]] = set()
434 
435 
436 def cancel_inflight() -> None:
437 for task in inflight:
438 task.cancel()
439 
440 
441 async def handle(raw: bytes, headers: dict[str, str]) -> tuple[dict[str, str], int]:
442 try:
443 event = client.beta.webhooks.unwrap(raw.decode(), headers=headers)
444 except standardwebhooks.WebhookVerificationError:
445 return {"error": "signature verification failed"}, 401
410446 if event.data.type != "session.status_run_started":
411 return {"status": "ignored"}
447 return {"status": "ignored"}, 200
448 loop = asyncio.get_running_loop()
449 for signum in (signal.SIGINT, signal.SIGTERM):
450 loop.add_signal_handler(signum, cancel_inflight)
451 task = asyncio.create_task(run_queued_work())
452 inflight.add(task)
453 task.add_done_callback(inflight.discard)
454 try:
455 await task
456 except asyncio.CancelledError:
457 return {"status": "shutting down"}, 503
458 return {"status": "ok"}, 200
459 
460 
461 async def run_queued_work() -> None:
412462 async for work in client.beta.environments.work.poller(
413463 environment_id=environment_id,
414464 environment_key=environment_key,
from line 472
422472 environment_id=environment_id,
423473 session_id=work.data.id,
424474 environment_key=environment_key,
475 # The per-session secret is what lets the worker mount the session's memory stores.
476 work_secret=work.secret,
425477 )
426 return {"status": "ok"}
427478 ```
428479 
429480 ```typescript TypeScript
from line 485
434485 const client = new Anthropic({
435486 authToken: environmentKey
436487 });
488 // Aborted on SIGINT or SIGTERM so an in-flight work item can upload changed memory
489 // files and remove its store directories before the process exits.
490 const shutdown = new AbortController();
491 process.once("SIGINT", () => shutdown.abort());
492 process.once("SIGTERM", () => shutdown.abort());
437493 
438494 export async function handle(req: Request): Promise<Response> {
439495 const body = await req.text();
from line 509
453509 blockMs: null,
454510 reclaimOlderThanMs: 2000,
455511 drain: true,
456 autoStop: false
512 autoStop: false,
513 signal: shutdown.signal
457514 })) {
458515 await client.beta.environments.work.worker({ workdir: "/workspace" }).handleItem({
459516 workId: work.id,
460517 environmentId,
461518 sessionId: work.data.id,
462 environmentKey
519 environmentKey,
520 // The per-session secret is what lets the worker mount the session's memory stores.
521 workSecret: work.secret ?? undefined,
522 signal: shutdown.signal
463523 });
464524 }
465525 return Response.json({ status: "ok" });
from line 537
477537 import (
478538 "context"
479539 "encoding/json"
540 "errors"
480541 "io"
481542 "log/slog"
482543 "net/http"
483544 "os"
545 "os/signal"
546 "syscall"
484547 
485548 "github.com/anthropics/anthropic-sdk-go"
486549 "github.com/anthropics/anthropic-sdk-go/lib/environments"
from line 561
498561 worker = environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
499562 Workdir: "/workspace",
500563 })
564 // Cancelled on SIGINT or SIGTERM (set in main) so an in-flight work item can
565 // upload changed memory files and remove its store directories before exit.
566 shutdown context.Context
501567 )
502568 
503569 func handle(w http.ResponseWriter, r *http.Request) {
from line 585
519585 // The Go SDK does not provide a RunOne convenience: drain pending items
520586 // with WorkPoller and run each one with HandleItem.
521587 // Detach from r.Context(): the session can outlive the webhook delivery timeout.
522 ctx := context.Background()
588 // The process-wide shutdown context still ends the item cleanly on SIGTERM.
589 ctx := shutdown
523590 poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
524591 EnvironmentID: environmentID,
525592 EnvironmentKey: environmentKey,
526593 BlockMs: param.Null[int64](),
527594 ReclaimOlderThanMs: param.NewOpt[int64](2000),
528595 Drain: true,
596 AutoStop: param.NewOpt(false),
529597 })
530598 defer poller.Close()
531599 for poller.Next() {
from line 603
535603 EnvironmentID: item.EnvironmentID,
536604 SessionID: item.Data.ID,
537605 EnvironmentKey: environmentKey,
606 // The per-session secret is what lets the worker mount the session's memory stores.
607 WorkSecret: item.Secret,
538608 }); err != nil {
539609 slog.Error("handle work item", "work_id", item.ID, "err", err)
540610 http.Error(w, "internal error", http.StatusInternalServerError)
from line 620
550620 }
551621 
552622 func main() {
623 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
624 defer stop()
625 shutdown = ctx
626 
627 server := &http.Server{Addr: ":8080"}
553628 http.HandleFunc("POST /webhook", handle)
554 if err := http.ListenAndServe(":8080", nil); err != nil {
555 slog.Error("http server", "err", err)
556 os.Exit(1)
629 go func() {
630 if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
631 slog.Error("http server", "err", err)
632 os.Exit(1)
633 }
634 }()
635 // On a signal, stop accepting deliveries and return only after in-flight
636 // handlers, and therefore their work items' memory teardown, have finished.
637 <-ctx.Done()
638 if err := server.Shutdown(context.Background()); err != nil {
639 slog.Error("http shutdown", "err", err)
557640 }
558641 }
559642 
from line 669
586669* **`EnvironmentWorker`:** the out-of-the-box worker. Handles polling, setup, and execution end to end.
587670 
588671 * `.run()`: runs indefinitely, picking up sessions as they arrive.
589 * `.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.
672 * `.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. To let the session mount its [memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores), also pass the work item's `secret` as `work_secret` (`workSecret` in TypeScript, `WorkSecret` in Go) or set `ANTHROPIC_WORK_SECRET`; `ant beta:worker poll --on-work` does not set that variable, so read the secret from the work item JSON it writes to your script's standard input, as shown in [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session).
673 * `memory_sync_interval` (`memorySyncIntervalMs` in TypeScript, `MemorySyncInterval` in Go) and `memory_sync_deletes` (`memoryRemoteDeletes`, `MemorySyncDeletes`): how often attached memory stores reconcile with the server while the session runs, and whether files the agent deletes locally are also deleted from the store. See [Configure sync](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#configure-sync) for units, defaults, and how to disable memory support.
590674 
591675* **`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.
592676 
593677 * `drain`: whether to stop polling once the queue is empty rather than waiting for new work.
594678 * `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.
595679 * `reclaim_older_than_ms`: re-claim work items that were claimed but never acknowledged within this many milliseconds.
596 * `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.
680 * `auto_stop` (`autoStop` in TypeScript, `AutoStop` in Go): whether to post a stop signal for each work item once your loop body finishes with it. Turn it off when the process you launch for the session, rather than the loop body, runs the work item to completion.
597681 
598682* **`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.
599683 
from line 703
619703 from anthropic import AsyncAnthropic
620704 from anthropic.types.beta.environments import BetaSelfHostedWork
621705 
706 SANDBOX_ENV = (
707 "ANTHROPIC_ENVIRONMENT_ID",
708 "ANTHROPIC_ENVIRONMENT_KEY",
709 "ANTHROPIC_WORK_ID",
710 "ANTHROPIC_SESSION_ID",
711 "ANTHROPIC_WORK_SECRET",
712 "ANTHROPIC_BASE_URL", # forwarded only when set on this host
713 )
714 
622715 
623716 async def launch_container(work: BetaSelfHostedWork) -> None:
624 # Replace with your own per-session sandbox launcher. Pass
625 # ANTHROPIC_ENVIRONMENT_KEY into the launched sandbox, never
626 # your API key.
627717 print(f"claimed session {work.data.id}")
718 # Replace `docker run` with your own sandbox launcher. Forward the environment
719 # key (never your API key) and the work item's per-session secret: the worker
720 # inside needs the secret to mount the session's memory stores.
721 env = os.environ | {
722 "ANTHROPIC_WORK_ID": work.id,
723 "ANTHROPIC_SESSION_ID": work.data.id,
724 "ANTHROPIC_WORK_SECRET": work.secret or "",
725 }
726 forward = [arg for name in SANDBOX_ENV for arg in ("-e", name)]
727 launcher = await asyncio.create_subprocess_exec(
728 "docker", "run", "--rm", "--detach", *forward, "your-sdk-worker-image", env=env
729 )
730 await launcher.wait()
628731 
629732 
630733 async def main() -> None:
from line 746
643746 ```
644747 
645748 ```typescript TypeScript
749 import { spawn } from "node:child_process";
750 import { once } from "node:events";
646751 import Anthropic from "@anthropic-ai/sdk";
647752 import { WorkPoller } from "@anthropic-ai/sdk/helpers/beta/environments";
648753 import type { BetaSelfHostedWork } from "@anthropic-ai/sdk/resources/beta/environments";
649754 
755 const SANDBOX_ENV = [
756 "ANTHROPIC_ENVIRONMENT_ID",
757 "ANTHROPIC_ENVIRONMENT_KEY",
758 "ANTHROPIC_WORK_ID",
759 "ANTHROPIC_SESSION_ID",
760 "ANTHROPIC_WORK_SECRET",
761 "ANTHROPIC_BASE_URL" // forwarded only when set on this host
762 ];
763 
650764 const environmentKey = process.env.ANTHROPIC_ENVIRONMENT_KEY!;
651765 const environmentId = process.env.ANTHROPIC_ENVIRONMENT_ID!;
652766 const client = new Anthropic({ authToken: environmentKey });
653767 
654768 async function launchContainer(work: BetaSelfHostedWork): Promise<void> {
655 // Replace with your own per-session sandbox launcher. Pass
656 // ANTHROPIC_ENVIRONMENT_KEY into the launched sandbox, never
657 // your API key.
658769 console.log(`claimed session ${work.data.id}`);
770 // Replace `docker run` with your own sandbox launcher. Forward the environment
771 // key (never your API key) and the work item's per-session secret: the worker
772 // inside needs the secret to mount the session's memory stores.
773 const env = {
774 ...process.env,
775 ANTHROPIC_WORK_ID: work.id,
776 ANTHROPIC_SESSION_ID: work.data.id,
777 ANTHROPIC_WORK_SECRET: work.secret ?? ""
778 };
779 const forward = SANDBOX_ENV.flatMap((name) => ["-e", name]);
780 const launcher = spawn(
781 "docker",
782 ["run", "--rm", "--detach", ...forward, "your-sdk-worker-image"],
783 { env, stdio: "inherit" }
784 );
785 await once(launcher, "close");
659786 }
660787 
661788 const poller = new WorkPoller({
from line 810
683810 "fmt"
684811 "log"
685812 "os"
813 "os/exec"
686814 
687815 "github.com/anthropics/anthropic-sdk-go"
688816 "github.com/anthropics/anthropic-sdk-go/lib/environments"
689817 "github.com/anthropics/anthropic-sdk-go/option"
818 "github.com/anthropics/anthropic-sdk-go/packages/param"
690819 )
691820 
692 func launchContainer(work *anthropic.BetaSelfHostedWork) {
693 // Replace with your own per-session sandbox launcher. The Go poller
694 // calls work.Stop when this function returns (it has no auto-stop
695 // opt-out), so block here until the session completes rather than
696 // detaching as the Python and TypeScript tabs do.
821 var sandboxEnv = []string{
822 "ANTHROPIC_ENVIRONMENT_ID",
823 "ANTHROPIC_ENVIRONMENT_KEY",
824 "ANTHROPIC_WORK_ID",
825 "ANTHROPIC_SESSION_ID",
826 "ANTHROPIC_WORK_SECRET",
827 "ANTHROPIC_BASE_URL", // forwarded only when set on this host
828 }
829 
830 func launchContainer(ctx context.Context, work *anthropic.BetaSelfHostedWork) error {
697831 fmt.Printf("claimed session %s\n", work.Data.ID)
832 // Replace `docker run` with your own sandbox launcher. Forward the environment
833 // key (never your API key) and the work item's per-session secret: the worker
834 // inside needs the secret to mount the session's memory stores.
835 args := []string{"run", "--rm", "--detach"}
836 for _, name := range sandboxEnv {
837 args = append(args, "-e", name)
838 }
839 launcher := exec.CommandContext(ctx, "docker", append(args, "your-sdk-worker-image")...)
840 launcher.Env = append(os.Environ(),
841 "ANTHROPIC_WORK_ID="+work.ID,
842 "ANTHROPIC_SESSION_ID="+work.Data.ID,
843 "ANTHROPIC_WORK_SECRET="+work.Secret,
844 )
845 launcher.Stdout, launcher.Stderr = os.Stdout, os.Stderr
846 return launcher.Run()
698847 }
699848 
700849 func main() {
from line 857
708857 poller := environments.NewWorkPoller(ctx, client, environments.WorkPollerOptions{
709858 EnvironmentID: environmentID,
710859 EnvironmentKey: environmentKey,
860 AutoStop: param.NewOpt(false), // the launched sandbox owns the stop call
711861 })
712862 defer poller.Close()
713863 
from line 865
715865 if err != nil {
716866 log.Fatal(err)
717867 }
718 launchContainer(work)
868 if err := launchContainer(ctx, work); err != nil {
869 log.Fatal(err)
870 }
719871 }
720872 }
721873 ```
from line 888
736888 ```
737889</CodeGroup>
738890 
739**`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`).
891Whatever launches the sandbox must forward the claimed work item's `secret` into it (for example as `ANTHROPIC_WORK_SECRET`) alongside the session, work, and environment identifiers, so the worker inside can mount the session's [memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores); see [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session).
892 
893**`AgentToolContext`** is the execution context for tool calls. It defines the working directory and path policy, and can download the session's skills. The file tools (`read`, `write`, `edit`, `glob`, `grep`) are confined to the working directory plus any directories listed in `allowed_roots` (`allowedRoots` in TypeScript, `AllowedRoots` in Go), and `write` and `edit` additionally refuse paths under `read_only_roots` (`readOnlyRoots`, `ReadOnlyRoots`). `EnvironmentWorker` adds the session's memory store directories to these lists itself. The confinement is a guardrail for the file tools only, not a sandbox; it does not constrain `bash`. **`beta_agent_toolset_20260401(env)`** takes an `AgentToolContext` and returns the standard tool implementations (`bash`, `read`, `write`, `edit`, `glob`, `grep`).
740894 
741895**With `EnvironmentWorker`:** both are managed automatically. Pass a `tools` factory to customize the tool list:
742896 
from line 1097
9431097</CodeGroup>
9441098 
9451099<Note>
946 Self-hosted sandboxes don't support `resources` entries; a session that includes any resource on a self-hosted environment is rejected.
1100 Self-hosted sandboxes support `memory_store` resources only; see [Use memory stores](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#use-memory-stores). A session on a self-hosted environment that includes a `file` or `github_repository` resource is rejected with a 400 error:
1101 
1102 ```text wrap
1103 Environment env_... is a self-hosted environment. `resources` are not supported with self-hosted environments.
1104 ```
1105 
1106 [Deployments](https://platform.claude.com/docs/en/managed-agents/scheduled-deployments) that target a self-hosted environment follow the same rule.
9471107</Note>
9481108 
9491109See [Self-hosted worker](https://platform.claude.com/docs/en/managed-agents/reference#self-hosted-worker) in the reference for the full list of CLI flags, and [SDK helpers](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#sdk-helpers) for the SDK helper options.
1110 
1111## Use memory stores
1112 
1113Sessions 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. On a self-hosted environment the SDK worker, rather than Anthropic's infrastructure, materializes each store for the agent, so memory stores there require `EnvironmentWorker` (or its `handle_item()` method) from the Python, TypeScript, or Go SDK.
1114 
1115The `ant` CLI worker (`ant beta:worker poll` and `ant beta:worker run`) does not mount memory stores. To combine the CLI poller with memory stores, run the SDK worker inside a per-session sandbox as described in [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session).
1116 
1117Memory 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).
1118 
1119### How the worker handles memory
1120 
1121When the worker claims a work item whose session has memory stores attached, it:
1122 
11231. Downloads each attached store to its `mount_path` on the worker host, authenticating with the work item's per-session `secret`. The `mount_path` is the same directory under `/mnt/memory/` that cloud sessions use (for example, `/mnt/memory/user-preferences/` for a store named "User Preferences"), and the session's system prompt describes it to the agent.
11242. Adds those directories to the file tools' allowed roots, and the directories of stores attached with `access: "read_only"` to their read-only roots, so the agent works on memories with the same `read`, `write`, `edit`, `glob`, and `grep` tools it uses in the working directory.
11253. Reconciles local and remote 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.
11264. Runs a final sync when the session ends, flushes any uploads still pending for up to 30 seconds, and then removes the directories it created. A worker that is cancelled while a session runs skips the final sync but still uploads changed files and removes the directories before it exits.
1127 
1128The 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, and 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, typically well under a minute at the default interval; sessions on cloud sandboxes see each other's changes almost immediately.
1129 
1130Each 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.
1131 
1132### Prepare the host
1133 
1134Memory stores on self-hosted sandboxes need a POSIX filesystem on the worker host (the Linux host from [Before you begin](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#before-you-begin)); 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.
1135 
1136Before you start the worker, create the parent directory and make it writable by the user the worker runs as:
1137 
1138```bash
1139sudo mkdir -p /mnt/memory && sudo chown "$USER" /mnt/memory
1140```
1141 
1142Do 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, refuses to start the session's work if something already exists at that path, and removes the directory when the session ends. Two operating rules follow:
1143 
1144* **Run one session per filesystem when sessions attach the same store.** Two sessions cannot mount the same store on one host at the same time, because both need the same path. Giving each session its own sandbox, as described in [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session), satisfies this rule.
1145* **Stop workers gracefully.** When you stop a worker while a session runs, `EnvironmentWorker` uploads the session's changed memory files and removes its store directories only if it is cancelled rather than killed: a killed process runs no teardown, and the worker does not install signal handlers itself. Wire SIGTERM and SIGINT to cancellation in the process that runs it: abort the `signal` you pass to the worker in TypeScript, cancel the context in Go, and in Python cancel the task that runs `run()` or `handle_item()` from a signal handler. Then stop workers with SIGTERM and give them at least 30 seconds to exit before any hard kill, because the final upload can take that long. If a worker is killed before its teardown runs, remove the leftover store directory under `/mnt/memory/` before the next session that attaches that store; any edits in it that had not synced are lost.
1146 
1147### Run one sandbox per session
1148 
1149The sandbox-per-session pattern in [Run a worker](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-a-worker) gives each session a fresh filesystem, which is what [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host) calls for when sessions attach the same store. Keep `ant beta:worker poll --on-work` (or the SDK's work poller) as the poller on the host.
1150 
1151The `ant beta:worker run` entrypoint shown there does not mount memory stores, so build the per-session image around the SDK worker instead: its entrypoint constructs `EnvironmentWorker` and calls `handle_item()` (`handleItem` in TypeScript, `HandleItem` in Go), which reads the session, work, and environment identifiers from the `ANTHROPIC_*` variables and the work item's per-session `secret` from `ANTHROPIC_WORK_SECRET`. You can also pass the secret explicitly as `work_secret` (`workSecret` in TypeScript, `WorkSecret` in Go).
1152 
1153<CodeGroup exclude="shell">
1154 ```python Python
1155 import asyncio
1156 import contextlib
1157 import os
1158 import signal
1159 from anthropic import AsyncAnthropic
1160 from anthropic.lib.environments import EnvironmentWorker
1161 
1162 
1163 async def main() -> None:
1164 async with AsyncAnthropic(auth_token=os.environ["ANTHROPIC_ENVIRONMENT_KEY"]) as client:
1165 worker = EnvironmentWorker(client, workdir="/workspace")
1166 # With no arguments, handle_item() reads the ANTHROPIC_* variables the spawn
1167 # script forwarded, including ANTHROPIC_WORK_SECRET.
1168 task = asyncio.create_task(worker.handle_item())
1169 # Cancelling the task when the container is stopped lets the worker upload
1170 # changed memory files and remove the store directories before it exits.
1171 loop = asyncio.get_running_loop()
1172 for signum in (signal.SIGINT, signal.SIGTERM):
1173 loop.add_signal_handler(signum, task.cancel)
1174 with contextlib.suppress(asyncio.CancelledError):
1175 await task
1176 
1177 
1178 asyncio.run(main())
1179 ```
1180 
1181 ```typescript TypeScript
1182 import Anthropic from "@anthropic-ai/sdk";
1183 import { EnvironmentWorker } from "@anthropic-ai/sdk/helpers/beta/environments";
1184 
1185 const client = new Anthropic({ authToken: process.env.ANTHROPIC_ENVIRONMENT_KEY });
1186 const controller = new AbortController();
1187 // Aborting when the container is stopped lets the worker upload changed memory
1188 // files and remove the store directories before it exits.
1189 process.once("SIGTERM", () => controller.abort());
1190 process.once("SIGINT", () => controller.abort());
1191 
1192 // With no arguments, handleItem() reads the ANTHROPIC_* variables the spawn
1193 // script forwarded, including ANTHROPIC_WORK_SECRET.
1194 await new EnvironmentWorker({
1195 client,
1196 workdir: "/workspace",
1197 signal: controller.signal
1198 }).handleItem();
1199 ```
1200 
1201 ```csharp C#
1202 // EnvironmentWorker is not currently available in the C# SDK.
1203 ```
1204 
1205 ```go Go
1206 package main
1207 
1208 import (
1209 "context"
1210 "log"
1211 "os"
1212 "os/signal"
1213 "syscall"
1214 
1215 "github.com/anthropics/anthropic-sdk-go"
1216 "github.com/anthropics/anthropic-sdk-go/lib/environments"
1217 "github.com/anthropics/anthropic-sdk-go/option"
1218 )
1219 
1220 func main() {
1221 // Cancelling the context when the container is stopped lets the worker upload
1222 // changed memory files and remove the store directories before it exits.
1223 ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
1224 defer stop()
1225 
1226 client := anthropic.NewClient(option.WithAuthToken(os.Getenv("ANTHROPIC_ENVIRONMENT_KEY")))
1227 worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
1228 Workdir: "/workspace",
1229 })
1230 // With zero-value options, HandleItem reads the ANTHROPIC_* variables the spawn
1231 // script forwarded, including ANTHROPIC_WORK_SECRET.
1232 if err := worker.HandleItem(ctx, environments.HandleItemOptions{}); err != nil {
1233 log.Fatalf("worker: %v", err)
1234 }
1235 }
1236 
1237 ```
1238 
1239 ```java Java
1240 // EnvironmentWorker is not currently available in the Java SDK.
1241 ```
1242 
1243 ```php PHP
1244 // EnvironmentWorker is not currently available in the PHP SDK.
1245 ```
1246 
1247 ```ruby Ruby
1248 # EnvironmentWorker is not currently available in the Ruby SDK.
1249 ```
1250</CodeGroup>
1251 
1252`ant beta:worker poll --on-work` does not set `ANTHROPIC_WORK_SECRET` for the script it spawns, so the spawn script reads the secret from the work item JSON on its standard input and passes it into the sandbox:
1253 
1254```bash
1255#!/bin/bash
1256# spawn.sh: called once per claimed work item
1257# The claimed work item arrives as JSON on stdin. Its secret is the
1258# per-session credential that the memory store endpoints require.
1259ANTHROPIC_WORK_SECRET="$(jq -r '.secret // empty')"
1260export ANTHROPIC_WORK_SECRET
1261mkdir -p "/host/outputs/$ANTHROPIC_SESSION_ID"
1262exec docker run --rm \
1263 -e ANTHROPIC_SESSION_ID -e ANTHROPIC_ENVIRONMENT_KEY \
1264 -e ANTHROPIC_WORK_ID -e ANTHROPIC_ENVIRONMENT_ID -e ANTHROPIC_BASE_URL \
1265 -e ANTHROPIC_WORK_SECRET \
1266 -v "/host/outputs/$ANTHROPIC_SESSION_ID":/workspace \
1267 your-sdk-worker-image
1268```
1269 
1270If you claim work with the SDK's work poller instead, pass each claimed item's `secret` into the sandbox you launch in the same way. Pass it only into the sandbox that serves that session, and never log it.
1271 
1272The sandbox image also needs a writable `/mnt/memory` (see [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host)). Because each sandbox serves one session and is discarded afterward, no leftover directories need cleanup, and the memory directories do not need to be bind-mounted to the host: the worker uploads their contents to the store before the sandbox exits. If you stop a container before its session ends, send a signal that the entrypoint turns into cancellation (see [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host)) rather than killing it, so that upload still runs. Give the container time to finish the upload as well: Docker follows the stop signal with SIGKILL after 10 seconds by default, so raise that limit to at least the 30 seconds that Prepare the host calls for, with `--stop-timeout` on `docker run` or your orchestrator's termination grace period.
1273 
1274### Configure sync
1275 
1276Two `EnvironmentWorker` options control memory behavior:
1277 
1278* **`memory_sync_interval`** (Python, in seconds; `memorySyncIntervalMs` in TypeScript, in milliseconds; `MemorySyncInterval` in Go, a duration): how often attached stores reconcile with the server while the session runs. Defaults to 15 seconds; the minimum is 5 seconds. A shorter interval narrows the window in which another session sees stale memories, at the cost of more memory store requests. `None` in Python, `null` in TypeScript, or a negative duration in Go disables memory support entirely: the worker neither downloads nor syncs stores, and a session with memory stores attached runs without them even though its system prompt still describes them, so disable memory support only on workers whose sessions attach no memory stores. While memory support is enabled, a work item that arrives without a per-session `secret` for a session with attached stores fails rather than running without memory (see [Troubleshoot memory mounts](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#troubleshoot-memory-mounts)).
1279* **`memory_sync_deletes`** (`memoryRemoteDeletes` in TypeScript, `MemorySyncDeletes` in Go): whether a file the agent deletes locally is also deleted from the store. The value is one of `"enabled"` (the default), `"log_only"`, or `"disabled"` in Python and TypeScript, and one of the constants `environments.MemorySyncDeletesEnabled` (the zero value), `environments.MemorySyncDeletesLogOnly`, or `environments.MemorySyncDeletesDisabled` in Go. When enabled, the worker deletes the memory from the store once a later sync confirms the file is still gone; in log-only mode it runs the same checks but only logs what it would have deleted, which lets you watch what your workers would delete before you trust the enabled mode; when disabled, it never deletes from the store. Uploads and downloads are unaffected by this setting.
1280 
1281Set these options where you construct the worker, whether through the `EnvironmentWorker` constructor or, in Python and TypeScript, the `client.beta.environments.work.worker()` factory that the webhook handler uses.
1282 
1283For example, to sync every 10 seconds and only log the deletes the worker would have made:
1284 
1285<CodeGroup exclude="shell">
1286 ```python Python
1287 worker = EnvironmentWorker(
1288 client,
1289 environment_id=environment_id,
1290 environment_key=environment_key,
1291 workdir="/workspace",
1292 memory_sync_interval=10, # seconds
1293 memory_sync_deletes="log_only",
1294 )
1295 ```
1296 
1297 ```typescript TypeScript
1298 const worker = new EnvironmentWorker({
1299 client,
1300 environmentId,
1301 environmentKey,
1302 workdir: "/workspace",
1303 memorySyncIntervalMs: 10_000,
1304 memorySyncDeletes: "log_only"
1305 });
1306 ```
1307 
1308 ```csharp C#
1309 // EnvironmentWorker is not currently available in the C# SDK.
1310 ```
1311 
1312 ```go Go
1313 worker := environments.NewEnvironmentWorker(client, environments.EnvironmentWorkerOptions{
1314 EnvironmentID: environmentID,
1315 EnvironmentKey: environmentKey,
1316 Workdir: "/workspace",
1317 MemorySyncInterval: 10 * time.Second,
1318 MemorySyncDeletes: environments.MemorySyncDeletesLogOnly,
1319 })
1320 ```
1321 
1322 ```java Java
1323 // EnvironmentWorker is not currently available in the Java SDK.
1324 ```
1325 
1326 ```php PHP
1327 // EnvironmentWorker is not currently available in the PHP SDK.
1328 ```
1329 
1330 ```ruby Ruby
1331 # EnvironmentWorker is not currently available in the Ruby SDK.
1332 ```
1333</CodeGroup>
1334 
1335### Read-only stores and conflicts
1336 
1337For a store attached with `access: "read_only"`, the `write` and `edit` tools refuse to change files inside its directory, and the worker never uploads anything from it. Changes made through `bash` are not blocked locally: they are never synced to the store, and the next remote change to that memory overwrites them. If you need the local copy itself to stay unchanged during the session, disable the `bash` tool for that agent; do not mount the store path read-only, because the worker itself must create the directory and write the downloaded memories into it.
1338 
1339Conflicts resolve in favor of the store. When the agent changes a memory file that also changed in the store since the session last synced it, the worker keeps the store's version at the next sync, 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.
1340 
1341### Troubleshoot memory mounts
1342 
1343The worker logs mount and background sync failures rather than reporting them to the session; only read-only refusals reach the agent, as tool errors (see [Read-only stores and conflicts](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#read-only-stores-and-conflicts)). If a memory store cannot be mounted when the worker claims a session, the worker fails the work item: the session emits no error event and stays idle.
1344 
1345| Symptom | Cause | Fix |
1346| --------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1347| The worker log contains `the work item carried no sessions token` (in Go, the `ErrSessionMemoryNoToken` error) and the work item fails. | The work item's per-session `secret` did not reach the worker: memory stores on self-hosted sandboxes are not enabled for your organization, or your spawn script did not forward the secret into the sandbox. | In the sandbox-per-session pattern, forward `ANTHROPIC_WORK_SECRET` into the sandbox as shown in [Run one sandbox per session](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#run-one-sandbox-per-session). If the worker polls and runs sessions in one process and still logs this, contact support. |
1348| The worker log contains `something already exists at the memory store's path`. | A directory left over from a previous session, usually one whose worker was killed before its teardown ran. | Remove the leftover directory that the log line names. Edits in it that had not synced are lost. |
1349| The worker log contains `cannot create the memory store's folder` and `the worker host must make this mount path writable`. | The user the worker runs as cannot create directories under `/mnt/memory`. | Create `/mnt/memory` and `chown` it to that user; see [Prepare the host](https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes#prepare-the-host). |
1350| The session sits `idle` with a `requires_action` stop reason and no error event shortly after a worker claimed it. | The worker failed the work item because it could not mount a memory store, for one of the preceding reasons. | Fix the cause on the host, then send a [`user.interrupt`](https://platform.claude.com/docs/en/managed-agents/events-and-streaming#integrating-events) event: the session's work is queued again and the next worker that claims it retries the mount. |
9501351 
9511352## Serve custom tools from your sandbox
9521353 
9531354 
Feedback