Claude Managed Agents on Amazon EKS — Self-Hosted Sandboxes¶
Anthropic Managed Agents is a hosted agent loop: Claude, conversation state, and a per-environment work queue all run on Anthropic's control plane. With self-hosted sandboxes, the orchestration stays on Anthropic's side but tool and code execution moves into infrastructure you control — inside your VPC, behind your egress policy, under your audit controls. The agent's files, processes, and network traffic stay in your environment.
This guide deploys that execution layer on Amazon EKS using the kubernetes-sigs/agent-sandbox project. It adapts Anthropic's reference architecture (originally published for GKE Agent Sandbox) to EKS with Karpenter, gVisor isolation, EKS-native storage, and Kubernetes network policies.
Preview software
Anthropic self-hosted sandboxes and the Agent Sandbox project are both early-stage. Their APIs may change without notice. Use this as a starting point for your own deployment, not a production-hardened configuration.
When to self-host¶
Self-hosting is the right choice when the agent needs to:
- Operate on data that cannot leave your network boundary
- Reach internal services that are not publicly routable
- Run under your organization's own compliance and audit controls
If none of these apply, Anthropic's default managed cloud sandboxes are simpler.
Why a warm pool, not N long-lived workers¶
Anthropic's simplest reference is "run N copies of ant beta:worker poll." That works, but every replica is a long-lived process that holds the environment key and keeps an open egress to api.anthropic.com, and every session that lands on a replica shares the same /workspace.
The Agent Sandbox primitives split the responsibilities:
- One small dispatcher polls the queue. It holds no untrusted code — it only binds claims.
- Each work item gets a
SandboxClaim, which binds a pre-warmed pod from theSandboxWarmPoolin well under a second, so a new session never waits on an image pull. - One session per pod. The pod runs the worker for exactly one session and is torn down when the session ends. The blast radius of any single sandbox is one session.
- The warm pool refills behind it, keeping the next session fast.
Why Standard EKS + Karpenter (not Auto Mode)¶
gVisor requires installing the runsc containerd shim on nodes at boot. EKS Auto Mode doesn't allow custom AMIs or user-data, making that impossible. Standard EKS with Karpenter gives you custom node configuration (AL2023 user-data installs gVisor at boot), dedicated tainted NodePools so only sandbox workloads land on isolation-capable nodes, and fast autoscaling.
Architecture¶
┌───────────────────────────┐ ┌──────────────────────────────────────────────┐
│ Anthropic control plane │ │ Your Amazon EKS cluster · Agent Sandbox │
│ │ │ │
│ ┌──────────────┐ │ poll │ ┌────────────┐ │
│ │ work queue │◀────────┼──────────┼───│ dispatcher │ (holds env key, no │
│ └──────┬───────┘ │ │ └─────┬──────┘ untrusted code) │
│ │ session_id │ │ │ SandboxClaim │
│ │ │ │ ▼ │
│ │ │ │ ┌──────────────┐ binds ┌──────────────┐ │
│ │ │ │ │SandboxWarmPool│─────────▶│ worker pod │ │
│ │ │ ant │ └──────────────┘ │ (gVisor) │ │
│ ┌──────▼───────┐ │ worker │ │ ant worker │ │
│ │ Claude loop │◀────────┼──run─────┼──────────────────────────────│ run │ │
│ └──────────────┘ tool │ results │ └──────┬───────┘ │
│ calls │ │ │ │
└───────────────────────────┘ │ /mnt/session/outputs │
│ ▼ │
│ ┌───────────┐ │
│ │ gp3 PVC │ │
│ └───────────┘ │
└──────────────────────────────────────────────┘
Two Anthropic credentials are involved, and keeping them separate is the core of the security model:
| Credential | Prefix | Scope | Where it lives |
|---|---|---|---|
| Environment key | sk-ant-oat01-... |
Ack work + post tool results for one environment | Dispatcher and worker pods only |
| Org API key | sk-ant-api03-... |
Full API access | Your machine only — creates agents/sessions; never in the cluster |
How it works, end to end¶
- You create a session that targets your self-hosted environment. Anthropic enqueues it as a work item.
- The dispatcher long-polls the work queue, claims the item, and creates a
SandboxClaimthat binds a ready pod from the warm pool. - Late binding: the pod is already running before the dispatcher knows which session it will serve, so the session ID can't be a container env var. The worker image runs a tiny HTTP listener and blocks until the dispatcher POSTs
{session_id, work_id}to it. - The worker then execs
ant beta:worker run.antacks the work item, downloads the agent's skills and memory stores, runs each tool call locally, and streams results back toapi.anthropic.com. - When the session goes idle the worker exits, the controller deletes the sandbox, and the warm pool spins up a fresh replacement.
Prerequisites¶
- An Amazon EKS Standard cluster (1.30+) with Karpenter, the EBS CSI driver add-on, and a CNI that enforces
NetworkPolicy(the VPC CNI with network policy enabled, or Cilium). - The Agent Sandbox controller installed in the cluster (serves the
agents.x-k8s.ioandextensions.agents.x-k8s.ioCRDs). See kubernetes-sigs/agent-sandbox. The manifests here usev1alpha1; setSANDBOX_API_VERSION=v1beta1on the dispatcher if your controller serves v1beta1. kubectl,kustomize,docker buildx, and the AWS CLI on your PATH.-
An Anthropic account with Managed Agents access, your org API key, and the Anthropic CLI (
ant) installed locally:ANT_VERSION=1.9.1 ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') case "$(uname -s)" in Darwin) curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_macos_${ARCH}.zip" -o /tmp/ant.zip && sudo unzip -o /tmp/ant.zip ant -d /usr/local/bin && rm /tmp/ant.zip ;; Linux) curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${ANT_VERSION}/ant_${ANT_VERSION}_linux_${ARCH}.tar.gz" | sudo tar -xz -C /usr/local/bin ant ;; esac ant --versionIf the download 404s, check the Anthropic CLI releases for a newer version.
Set shared shell variables used throughout:
export CLUSTER_NAME=my-eks-cluster
export AWS_REGION=us-west-2
export ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
export ECR=${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
Module 1 — Create a self-hosted environment on Anthropic¶
An environment is the work queue that links Anthropic's agent loop to your cluster. Create one of type Self-hosted in the Anthropic Console (Environments → New → Self-hosted), or via the API:
export ANTHROPIC_API_KEY=sk-ant-api03-YOUR_ORG_KEY # org key, this command only
curl https://api.anthropic.com/v1/environments \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{"name":"eks-agent-sandbox","config":{"type":"self_hosted"}}'
Collect two values:
- Environment ID (
env_01...) — theidin the API response, or in the Console URL of the environment detail page. - Environment key (
sk-ant-oat01-...) — open the environment in the Console and click Generate environment key. You can only generate it in the Console.
Checkpoint: you have both an env_... ID and an sk-ant-oat01-... key.
Module 2 — Build and push the images¶
Two images: the worker (ships ant + the dispatch listener) and the dispatcher (polls the queue, binds claims). Both target linux/arm64 for Graviton.
aws ecr get-login-password --region "$AWS_REGION" \
| docker login --username AWS --password-stdin "$ECR"
for repo in claude-agent-worker claude-agent-dispatcher; do
aws ecr describe-repositories --repository-names "$repo" --region "$AWS_REGION" >/dev/null 2>&1 \
|| aws ecr create-repository --repository-name "$repo" --region "$AWS_REGION" >/dev/null
done
docker buildx build --platform linux/arm64 \
-t "$ECR/claude-agent-worker:latest" --push src/worker
docker buildx build --platform linux/arm64 \
-t "$ECR/claude-agent-dispatcher:latest" --push src/dispatcher
Checkpoint: both repositories list a latest image:
aws ecr list-images --repository-name claude-agent-worker --region "$AWS_REGION"
aws ecr list-images --repository-name claude-agent-dispatcher --region "$AWS_REGION"
Module 3 — Deploy the sandbox infrastructure¶
Store the environment key as a Secret (prefer this over committing it; for production, source it from AWS Secrets Manager via the Secrets Store CSI driver or External Secrets Operator):
kubectl create namespace claude-agents --dry-run=client -o yaml | kubectl apply -f -
kubectl -n claude-agents create secret generic anthropic-env-key \
--from-literal=ANTHROPIC_ENVIRONMENT_KEY="$ANTHROPIC_ENVIRONMENT_KEY"
Fill in the manifest placeholders, then apply the kustomization:
cd k8s
sed -i.bak "s|ACCOUNT_ID.dkr.ecr.us-west-2.amazonaws.com|$ECR|g" sandboxtemplate-worker.yaml dispatcher.yaml
sed -i.bak "s|ENV_ID|$ANTHROPIC_ENVIRONMENT_ID|g" dispatcher.yaml sandboxtemplate-worker.yaml
sed -i.bak "s|\${CLUSTER_NAME}|$CLUSTER_NAME|g" nodepool-gvisor.yaml
rm -f *.bak
kubectl apply -k .
Checkpoint: the dispatcher is polling and the warm pool has ready workers (give Karpenter ~2 minutes to provision a gVisor node on the first apply):
NAME REPLICAS
sandboxwarmpool.extensions.agents.x-k8s.io/claude-agent-worker 2
NAME READY STATUS RESTARTS AGE
pod/claude-agent-dispatcher-xxxxxxxxxx-xxxxx 1/1 Running 0 2m
pod/claude-agent-worker-xxxxx 1/1 Running 0 2m
pod/claude-agent-worker-yyyyy 1/1 Running 0 2m
If a worker stays Pending past three minutes, Karpenter is still provisioning a gVisor node — check kubectl -n claude-agents describe pod -l app=claude-agent-worker for scale-up events.
Module 4 — Pair a Managed Agent and verify¶
Authenticate the CLI, then unset the org API key so it doesn't shadow the OAuth token for interactive commands:
Create an agent and a session bound to your environment:
AGENT_ID=$(ant beta:agents create \
--name eks-sandbox-probe \
--model '{"id":"claude-sonnet-4-6"}' \
--system 'You run tools inside a self-hosted sandbox on EKS under gVisor. Use bash to inspect the environment when asked.' \
--tool '{"type":"agent_toolset_20260401"}' \
--format json --transform 'id' -r)
SESSION_ID=$(ant beta:sessions create \
--agent '{"id":"'"$AGENT_ID"'","type":"agent"}' \
--environment-id "$ANTHROPIC_ENVIRONMENT_ID" \
--format json --transform 'id' -r)
echo "AGENT_ID=$AGENT_ID SESSION_ID=$SESSION_ID"
Send a prompt that forces a tool call, wait, then read the answer:
ant beta:sessions:events send --session-id "$SESSION_ID" \
--event '{"type":"user.message","content":[{"type":"text","text":"Run `uname -a && hostname` and tell me what sandbox you are executing in."}]}'
sleep 15
ant beta:sessions:events list --session-id "$SESSION_ID" --order asc --format jsonl \
| jq -r 'select(.type=="agent.message") | .content[0].text'
Checkpoint: the model reports a gVisor kernel (the 4.4.0 string is gVisor's synthetic kernel signature) and a claude-agent-worker-* hostname — proof the bash ran on a pod in your cluster, not Anthropic's. Confirm from the cluster side:
Any session created with --environment-id "$ANTHROPIC_ENVIRONMENT_ID" now executes its tool calls here.
ant 1.9.1 flag parsing
--model and --agent accept a string per --help, but ant 1.9.1 only parses the JSON object form — pass '{"id":"..."}'. Prefer events list over events stream for inspection; stream only emits events that arrive after it connects.
Autoscaling the warm pool¶
The SandboxWarmPool ships at a fixed replicas: 2. For real traffic, scale it on queue depth. Two options:
- HPA on the
/scalesubresource.SandboxWarmPoolexposes/scale, so aHorizontalPodAutoscalercan target it with a custom/external metric. - A stats-adapter sidecar. A small process reads Anthropic's
work.statsendpoint every ~15 s and patches the warm pool's replica count to match concurrent sessions. This path needs the org API key — keep it in the dispatcher namespace, never in a worker pod. See Anthropic's GKE reference implementation for a workingstats-adapter.
Security model — what you own¶
When you self-host, Anthropic secures only the control plane. These are your responsibilities (see Anthropic's security model):
- Runtime hardening. This recipe runs workers as non-root with a read-only root filesystem, all capabilities dropped, no mounted service-account token, and gVisor isolation. Keep those defaults.
- Network egress. The default-deny
NetworkPolicy+ allow-list is the base layer. BecauseNetworkPolicycan't match a hostname, the HTTPS rule is a broad0.0.0.0/0:443. Narrow it toapi.anthropic.comwith a hostname-aware control — AWS Network Firewall FQDN rules, a CiliumtoFQDNspolicy, or an egress proxy. - Key storage and rotation. The environment key belongs in a secrets manager, not an image. Rotate it immediately on any suspected exposure — revocation takes effect on the worker's next call.
- Trust-boundary isolation. The environment key is scoped to one environment's queue. For untrusted multi-tenant workloads, provision a separate workspace and environment per trust boundary so each key maps to a single tenant's sessions.
- Session data. Conversation content and tool outputs flow through your worker and the
/mnt/session/outputsPVC. Retaining, redacting, or deleting that data is on you. - Memory stores. The worker keeps a working copy under
/mnt/memory/for the session and deletes it on teardown. A worker that exits without teardown can leave a copy behind — clean up leftover copies and keep that path isolated between sessions.
Clean up¶
# Delete Anthropic sessions
ant beta:sessions list --format jsonl | jq -r '.id' \
| xargs -I{} ant beta:sessions delete --session-id {}
# Remove cluster resources
kubectl -n claude-agents delete sandboxclaim --all
kubectl delete -k k8s
kubectl delete namespace claude-agents
Delete the ECR repositories and the self-hosted environment in the Anthropic Console if you no longer need them.
What's next¶
- Give the agent internal tools. Point your egress policy at your own internal services, or bake custom tools / an in-network MCP server into the worker image (
src/worker/). That is the whole point of self-hosting. - Harden egress to exactly
api.anthropic.comwith a hostname-aware control. - Add autoscaling so the warm pool tracks demand.