When embedding autonomous AI agents into a production service, the first wall you hit is drawing the line on how much freedom to grant them.
In April 2026, Anthropic released Claude Managed Agents in public beta, and it comes with a clear answer to this problem. Through a combination of container-based sandboxing, server-sent event (SSE) streaming, and built-in tools, the design balances agent autonomy with security.
But the official documentation alone doesn't cover the practical decisions you face in production: how to set container resource limits, what network access scope is appropriate, or how to handle runaway agents. This article draws on hands-on experience deploying Managed Agents in production to walk through sandbox security design patterns with concrete code.
The Three-Layer Sandbox Architecture
The Managed Agents sandbox isn't just a Docker container. It achieves defense-in-depth through three distinct layers.
import anthropic
client = anthropic.Anthropic()
# Specify sandbox settings when creating an agent
agent = client.agents.create(
model="claude-sonnet-4-6",
instructions="An agent that performs data analysis tasks",
tools=[
{"type": "code_execution"},
{"type": "file_read"},
{"type": "file_write"},
],
# Sandbox configuration
sandbox={
"type": "managed",
"memory_mb": 512,
"timeout_seconds": 300,
"network_access": "restricted",
"allowed_domains": [
"api.example.com",
"storage.googleapis.com"
]
}
)Layer 1: Container isolation means each agent session runs in an independent Linux container. Filesystems are completely isolated—files created in one session are invisible to another.
Layer 2: Resource limits cap memory, CPU, and execution time. The code above specifies 512MB of memory and a 300-second timeout, but choosing the right values takes some thought.
Layer 3: Network policy is the most critical. Setting network_access to "restricted" blocks all outbound traffic except to domains explicitly listed in allowed_domains. There's almost never a reason to use "full" in production.
Practical Resource Limit Configuration
The official docs state that the default memory_mb is 1024MB, but in practice, the optimal value varies dramatically by task type.
# Resource configuration templates by task type
SANDBOX_PROFILES = {
# Text processing (summarization, classification, transformation)
"text_processing": {
"memory_mb": 256,
"timeout_seconds": 120,
"network_access": "none",
},
# Data analysis (CSV/JSON parsing, chart generation)
"data_analysis": {
"memory_mb": 1024,
"timeout_seconds": 600,
"network_access": "restricted",
"allowed_domains": ["storage.googleapis.com"],
},
# Code generation and execution
"code_execution": {
"memory_mb": 2048,
"timeout_seconds": 900,
"network_access": "restricted",
"allowed_domains": ["pypi.org", "registry.npmjs.org"],
},
# External API integration
"api_integration": {
"memory_mb": 512,
"timeout_seconds": 300,
"network_access": "restricted",
# allowed_domains set dynamically per task
},
}
def create_agent_with_profile(profile_name: str, extra_domains: list[str] | None = None):
"""Create an agent based on a predefined profile"""
config = SANDBOX_PROFILES[profile_name].copy()
if extra_domains and config.get("network_access") == "restricted":
config.setdefault("allowed_domains", [])
config["allowed_domains"].extend(extra_domains)
return client.agents.create(
model="claude-sonnet-4-6",
instructions=f"Profile: {profile_name}",
tools=[{"type": "code_execution"}],
sandbox={"type": "managed", **config},
)Pay attention to the "text_processing" profile's "network_access": "none". Text processing tasks rarely need external communication, and completely blocking the network minimizes security risk. The principle of least privilege applies just as strongly to agent design.
Real-Time Monitoring with SSE Streaming
A standout feature of Managed Agents is the ability to get real-time execution status via SSE (Server-Sent Events). This enables anomaly detection and automatic shutdown mechanisms.
import json
from collections import defaultdict
class AgentMonitor:
"""Monitors agent execution and detects anomalies"""
def __init__(self, max_tool_calls: int = 50, max_errors: int = 3):
self.max_tool_calls = max_tool_calls
self.max_errors = max_errors
self.tool_call_count = 0
self.error_count = 0
self.accessed_files: list[str] = []
def process_event(self, event) -> dict:
"""Process an SSE event and return warnings if anomalies detected"""
result = {"action": "continue", "warnings": []}
if event.type == "tool_use":
self.tool_call_count += 1
# Check tool call count limit
if self.tool_call_count > self.max_tool_calls:
result["action"] = "stop"
result["warnings"].append(
f"Tool call limit exceeded: {self.tool_call_count}/{self.max_tool_calls}"
)
return result
# Track file access
if event.tool_name in ("file_read", "file_write"):
path = event.tool_input.get("path", "")
self.accessed_files.append(path)
# Detect access to sensitive paths
sensitive_patterns = ["/etc/", "/proc/", "/sys/", ".env", "credentials"]
for pattern in sensitive_patterns:
if pattern in path:
result["warnings"].append(
f"Sensitive path access detected: {path}"
)
elif event.type == "error":
self.error_count += 1
if self.error_count >= self.max_errors:
result["action"] = "stop"
result["warnings"].append(
f"Error limit reached: {self.error_count}/{self.max_errors}"
)
return result
async def run_monitored_agent(agent_id: str, task: str):
"""Run an agent with monitoring"""
monitor = AgentMonitor(max_tool_calls=30, max_errors=2)
session = client.agents.sessions.create(agent_id=agent_id)
with client.agents.sessions.turn_stream(
session_id=session.id,
messages=[{"role": "user", "content": task}],
) as stream:
for event in stream:
check = monitor.process_event(event)
if check["warnings"]:
for w in check["warnings"]:
print(f"⚠️ {w}")
# In production, send notifications to Slack/PagerDuty here
if check["action"] == "stop":
print("🛑 Force-stopping agent")
client.agents.sessions.cancel(session_id=session.id)
break
return {
"tool_calls": monitor.tool_call_count,
"errors": monitor.error_count,
"files_accessed": monitor.accessed_files,
}The key here is the process_event method, which runs three checks on every tool call: count limits, file path safety validation, and cumulative error tracking. The moment any threshold is crossed, the session is immediately cancelled.
In production, we layer on additional logic: monitoring the moving average of tool execution time to detect sudden latency spikes, and detecting repeated calls to the same tool (a sign of infinite loops).
Failsafe Design: When Agents Go Off the Rails
No matter how carefully you design your sandbox, unexpected behavior will happen. What matters is having mechanisms ready to minimize the blast radius.
import asyncio
from contextlib import asynccontextmanager
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class SessionGuard:
"""Guards managing session lifecycle"""
session_id: str
started_at: datetime = field(default_factory=datetime.now)
max_duration_seconds: int = 600
max_output_bytes: int = 10 * 1024 * 1024 # 10MB
total_output_bytes: int = 0
@property
def elapsed_seconds(self) -> float:
return (datetime.now() - self.started_at).total_seconds()
@property
def is_expired(self) -> bool:
return self.elapsed_seconds > self.max_duration_seconds
def track_output(self, data: str) -> bool:
"""Track output data, return False if limit exceeded"""
self.total_output_bytes += len(data.encode("utf-8"))
return self.total_output_bytes <= self.max_output_bytes
@asynccontextmanager
async def guarded_session(agent_id: str, **guard_kwargs):
"""Context manager for guarded agent sessions"""
session = client.agents.sessions.create(agent_id=agent_id)
guard = SessionGuard(session_id=session.id, **guard_kwargs)
# Launch timeout watchdog as a concurrent task
async def _watchdog():
while not guard.is_expired:
await asyncio.sleep(5)
print(f"⏰ Session {session.id} timed out")
try:
client.agents.sessions.cancel(session_id=session.id)
except Exception:
pass # Already terminated
watchdog_task = asyncio.create_task(_watchdog())
try:
yield session, guard
finally:
watchdog_task.cancel()
try:
client.agents.sessions.delete(session_id=session.id)
except Exception:
passSessionGuard manages both time limits and output volume limits simultaneously. The output volume limit is easy to overlook, but it's essential for the scenario where an agent keeps generating massive files.
The _watchdog coroutine checks for timeout every 5 seconds and force-cancels the session the moment the limit is exceeded. This is separate from the Managed Agents API's own timeout_seconds—having an independent timeout on the application side acts as a safety net in case the API-side control fails for any reason.
Production Deployment Checklist
Based on the patterns above, here's what to verify when deploying Managed Agents to production.
Network controls: Ensure network_access is set to "none" or "restricted" based on the task. Keep allowed_domains to the absolute minimum. When internal API access is needed, route it through a dedicated API gateway for agents.
Resource limits: Set memory_mb and timeout_seconds based on actual workload measurements. Run load tests to measure peak memory usage, then add a 20% margin.
Monitoring and alerts: Log SSE events and detect anomaly patterns (tool call spikes, consecutive errors). Visualize average session duration and success rates on a dashboard.
Failsafes: Application-side timeouts must function independently from the API side. Agent output volume must be capped. Emergency shutdown procedures (bulk session cancellation) must be included in runbooks.
These principles aren't specific to Managed Agents—they're universal for operating any autonomous AI agent. Thinking about what an agent shouldn't do is just as important as thinking about what it can do when it comes to production reliability.
This article is published in full as a free sample of our premium content. Premium articles on Claude Lab feature practical implementation patterns and production know-how like this, with working code examples. To access all premium articles, consider joining our membership.