Sandbox Execution

Focus Deep-Dive

Docker/SSH backends, workspace mounting, tool policies, and container lifecycle

Sandbox execution isolates tool operations inside Docker containers or SSH sessions, preventing the agent from modifying the host system directly. The sandbox manages workspace mounting, tool allow-lists, browser automation bridges, container lifecycle (startup, idle detection, cleanup), and ACP inheritance modes — all configured through a unified SandboxConfig.

📚 What is this & why does it matter?

When an LLM agent can execute arbitrary code and file operations, it must be sandboxed to prevent accidental (or adversarial) damage to the host. The sandbox module provides defense-in-depth: tool-level allow-lists restrict what the agent can do, while Docker/SSH isolation restricts where it can do it. Without sandboxing, a single rm -rf / from a confused model could destroy the host.

💡 Real-world analogy

The sandbox is like a chemistry lab’s fume hood. Scientists (agents) perform experiments (tool operations) inside the hood (container) rather than on the open bench (host). The hood has its own ventilation (workspace mounts), safety interlocks (tool policies), and an automatic shutoff timer (idle detection). If an experiment goes wrong, the damage is contained inside the hood, not spread across the entire lab.

Contents

Overview

Primary role

Isolate agent tool execution in Docker containers or SSH sessions with configurable tool policies and workspace mounts.

Main inputs

SandboxConfig (mode, backend, image, tool policies), agent workspace path, user cwd.

Key decisions

Sandbox mode (off/non-main/all), backend selection (Docker/SSH), scope (Session/Agent/Shared), tool allow-list.

Outputs / side effects

Running containers, workspace mounts, browser bridges, sandbox runtime info, cleanup on idle/expiry.

flowchart LR
  subgraph Config["SandboxConfig"]
    Mode["Mode: off | non-main | all"]
    Backend["Backend: Docker | SSH"]
    Policy["Tool Policy"]
  end
  subgraph Resolution["Sandbox Resolution"]
    Resolve["Resolve sandbox mode"]
    Select["Select backend"]
    Mount["Configure mounts"]
  end
  subgraph Execution["Container Lifecycle"]
    Start["Start container"]
    Tool["Execute tool"]
    Idle["Idle detection"]
    Cleanup["Cleanup / remove"]
  end
  Mode --> Resolve
  Backend --> Select
  Policy --> Select
  Resolve --> Select
  Select --> Mount
  Mount --> Start
  Start --> Tool
  Tool --> Idle
  Idle --> Cleanup

The sandbox resolution pipeline evaluates whether the current tool call requires sandboxing (based on mode), selects the appropriate backend, configures workspace mounts, starts or reuses a container, executes the tool inside it, and monitors for idle timeout or maximum age expiry.

Decision Points

DecisionWhat controls itWhy it matters
Whether to sandbox at allSandboxConfig.mode: off, non-main, or alloff = no isolation; non-main = sandbox spawned agents only; all = sandbox everything
Docker vs. SSH backendSandboxConfig.backendDocker is faster for local dev; SSH enables remote sandbox hosts
Container scopeScope: Session, Agent, or SharedSession = per run; Agent = shared within one agent; Shared = across all agents
Tool allow-listDEFAULT_TOOL_ALLOW arrayRestricts which tool categories are permitted inside the sandbox
Browser automationSandboxConfig.browser configEnables CDP bridge at port 9222 for browser-based tools
ACP inheritanceACP mode: inherit vs. requireinherit = use parent’s sandbox; require = must have own sandbox
Container lifecycleIdle hours + max age daysPrevents resource leaks from abandoned containers

How It Works

SandboxConfig & SandboxContext Types

The sandbox system is configured through two core types defined in sandbox/types.ts. SandboxConfig is the user-facing configuration; SandboxContext is the runtime state after a sandbox is created:

// sandbox/types.ts — core configuration types
interface SandboxConfig {
  mode: "off" | "non-main" | "all";   // When to sandbox
  backend: "docker" | "ssh";           // Isolation backend
  workspaceRoot?: string;              // Override workspace root
  dockerImage?: string;                // Custom Docker image
  sshConfig?: SshConfig;               // SSH backend settings
  browser?: BrowserConfig;             // Browser automation config
  toolPolicies?: ToolPolicy[];         // Per-tool overrides
}

interface SandboxContext {
  containerName: string;               // Running container ID/name
  workspaceMounts: WorkspaceMount[];   // Active bind mounts
  dockerConfig: DockerConfig;          // Resolved Docker settings
  browserBridgeUrls?: {                // CDP bridge endpoints
    http: string;                      // http://localhost:9222
    ws: string;                        // ws://localhost:9222
  };
  scope: "session" | "agent" | "shared";
  createdAt: Date;
  lastActivityAt: Date;
}

interface WorkspaceMount {
  hostPath: string;                    // Path on host machine
  containerPath: string;               // Path inside container
  readOnly: boolean;                   // Mount as read-only?
}

interface SshConfig {
  host: string;
  port: number;
  username: string;
  privateKeyPath?: string;
  password?: string;
}

Default Configuration Constants

The sandbox module provides sensible defaults for every configurable value:

// sandbox/defaults.ts — default constants
import path from "node:path";

// Workspace
const STATE_DIR = process.env.OPENCLAW_STATE_DIR ?? path.join(
  process.env.HOME ?? "/root", ".openclaw"
);
export const DEFAULT_SANDBOX_WORKSPACE_ROOT = path.join(STATE_DIR, "sandboxes");
export const DEFAULT_SANDBOX_IMAGE = "openclaw-sandbox:bookworm-slim";

// Browser automation
export const DEFAULT_SANDBOX_BROWSER_CDP_PORT = 9222;
export const DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS = 12000;

// Lifecycle
export const DEFAULT_SANDBOX_IDLE_HOURS = 24;
export const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;

// Tool allow-list
export const DEFAULT_TOOL_ALLOW = [
  "exec", "process",
  "read", "write", "edit", "apply_patch",
  "image",
  "sessions_list", "sessions_history",
  "sessions_send", "sessions_spawn", "sessions_yield",
  "subagents", "session_status",
];
Security note: The DEFAULT_TOOL_ALLOW list does not include network tools (fetch, curl) or destructive system tools (shutdown, reboot). These must be explicitly added to the allow-list if needed.

Workspace Mounting

The sandbox mounts two directories into the container: the agent workspace at /agent and the user’s current working directory at /workspace. The agent workspace is read-write (for state persistence); the user workspace defaults to read-only but can be made writable:

Key detail: The agent workspace (/agent) is where the sandbox stores intermediate files, tool outputs, and session state. The user workspace (/workspace) is the project directory the user asked the agent to work on. Keeping them separate prevents agent state from polluting the user’s project.

Container State Machine

Containers follow a deterministic lifecycle managed by the sandbox runtime:

stateDiagram-v2
  [*] --> Idle
  Idle --> Starting : tool request
  Starting --> InUse : container ready
  Starting --> Failed : startup timeout
  InUse --> Idle : tool complete
  InUse --> InUse : another tool request
  Idle --> Terminating : idle timeout or max age
  Terminating --> Removed : cleanup complete
  Failed --> Removed : cleanup
  Removed --> [*]

Containers transition through five states. The Idle → Starting transition occurs on the first tool request. Once In Use, subsequent tool requests reuse the running container. After the last tool completes, the container returns to Idle and starts an idle timer. If no new requests arrive within DEFAULT_SANDBOX_IDLE_HOURS (24h), or the container exceeds DEFAULT_SANDBOX_MAX_AGE_DAYS (7d), it transitions to Terminating and is removed.

Sandbox Scope

The scope determines container sharing across agent runs:

ScopeSharing modelUse case
sessionOne container per agent run; destroyed when run endsMaximum isolation; default for untrusted operations
agentOne container shared across all runs of the same agentState persistence between runs (e.g., database initialization)
sharedOne container shared across all agents in the sessionResource efficiency when multiple agents work on the same project

ACP Sandbox Modes

The Agent Context Protocol (ACP) defines two sandbox inheritance modes for child agents spawned via sessions-spawn-tool.ts:

// ACP sandbox inheritance modes
type AcpSandboxMode = "inherit" | "require";

interface AcpSpawnOptions {
  sandboxMode: AcpSandboxMode;
  // "inherit" — child uses parent's sandbox container
  //   Pro: no startup overhead, shared state
  //   Con: child can see parent's files
  // "require" — child gets its own sandbox
  //   Pro: full isolation from parent
  //   Con: container startup latency

  toolPolicies?: ToolPolicy[];  // Child-specific overrides
}

// Example: spawn a sandboxed child for untrusted code review
const child = await spawnAgent({
  sandboxMode: "require",       // Isolated sandbox
  toolPolicies: [
    { tool: "exec", allow: false },  // No code execution
    { tool: "read", allow: true },   // Read-only access
  ],
});
Security consideration: The inherit mode shares the parent’s container filesystem. A child agent with inherit can read and modify files created by the parent. Use require mode when the child handles untrusted input or performs potentially destructive operations.

Sandbox Resolution Flow

sequenceDiagram
  participant Agent as Agent Runtime
  participant Config as SandboxConfig
  participant Resolver as Sandbox Resolver
  participant Docker as Docker Backend
  participant Container as Container
  participant Tool as Tool Executor

  Agent->>Config: read sandbox settings
  Config-->>Agent: mode, backend, policies
  Agent->>Resolver: resolveSandbox(toolName, config)
  alt mode = "off"
    Resolver-->>Agent: no sandbox (host execution)
  else mode = "non-main" and isMainAgent
    Resolver-->>Agent: no sandbox (main agent exempt)
  else mode = "all" or non-main agent
    Resolver->>Docker: findOrCreateContainer(scope)
    alt Existing container available
      Docker-->>Resolver: reuse container
    else No container
      Docker->>Container: create + start
      Docker->>Container: mount workspaces
      Container-->>Docker: ready
      Docker-->>Resolver: new SandboxContext
    end
    Resolver->>Resolver: checkToolPolicy(toolName)
    alt Tool allowed
      Resolver->>Tool: execute inside container
      Tool-->>Resolver: result
      Resolver->>Container: update lastActivityAt
    else Tool denied
      Resolver-->>Agent: ToolAuthorizationError
    end
    Resolver-->>Agent: tool result
  end

The resolution flow first checks if sandboxing is required for this tool call. If yes, it finds or creates a container with the appropriate scope, verifies the tool is permitted by the allow-list, and executes inside the container. The container’s lastActivityAt is updated after every tool execution to reset the idle timer.

Annotated Code Walkthrough

Walk through setting up a sandbox, executing a tool inside it, and handling lifecycle events:

Build a SandboxConfig from user settings and defaults. Missing values fall back to the default constants:

// sandbox/config.ts — building sandbox configuration
import {
  DEFAULT_SANDBOX_WORKSPACE_ROOT,
  DEFAULT_SANDBOX_IMAGE,
  DEFAULT_SANDBOX_IDLE_HOURS,
  DEFAULT_SANDBOX_MAX_AGE_DAYS,
  DEFAULT_TOOL_ALLOW,
} from "./defaults.js";

export function buildSandboxConfig(
  userConfig: Partial<SandboxConfig>
): SandboxConfig {
  return {
    mode: userConfig.mode ?? "non-main",
    backend: userConfig.backend ?? "docker",
    workspaceRoot: userConfig.workspaceRoot ?? DEFAULT_SANDBOX_WORKSPACE_ROOT,
    dockerImage: userConfig.dockerImage ?? DEFAULT_SANDBOX_IMAGE,
    sshConfig: userConfig.sshConfig,
    browser: userConfig.browser ?? {
      cdpPort: DEFAULT_SANDBOX_BROWSER_CDP_PORT,
      autostartTimeoutMs: DEFAULT_SANDBOX_BROWSER_AUTOSTART_TIMEOUT_MS,
    },
    toolPolicies: userConfig.toolPolicies ?? DEFAULT_TOOL_ALLOW.map(
      tool => ({ tool, allow: true })
    ),
  };
}

Create the Docker container with workspace mounts and browser bridge. The container name encodes the scope and agent ID for easy identification:

// sandbox/docker-backend.ts — container creation
async function createContainer(
  config: SandboxConfig,
  agentId: string,
  scope: SandboxContext["scope"]
): Promise<SandboxContext> {
  const containerName = `openclaw-${scope}-${agentId}-${Date.now()}`;

  const mounts: WorkspaceMount[] = [
    {
      hostPath: path.join(config.workspaceRoot!, agentId),
      containerPath: "/agent",
      readOnly: false,
    },
    {
      hostPath: process.cwd(),
      containerPath: "/workspace",
      readOnly: true,  // User workspace is read-only by default
    },
  ];

  const dockerArgs = [
    "run", "-d",
    "--name", containerName,
    ...mounts.flatMap(m => [
      "-v", `${m.hostPath}:${m.containerPath}:${m.readOnly ? "ro" : "rw"}`,
    ]),
    ...(config.browser ? ["-p", `${config.browser.cdpPort}:9222`] : []),
    config.dockerImage!,
    "sleep", "infinity",  // Keep container alive
  ];

  await exec("docker", dockerArgs);

  return {
    containerName,
    workspaceMounts: mounts,
    dockerConfig: { image: config.dockerImage! },
    browserBridgeUrls: config.browser ? {
      http: `http://localhost:${config.browser.cdpPort}`,
      ws: `ws://localhost:${config.browser.cdpPort}`,
    } : undefined,
    scope,
    createdAt: new Date(),
    lastActivityAt: new Date(),
  };
}

Execute a tool inside the sandbox container. The executor translates the tool call into a Docker exec command:

// sandbox/executor.ts — tool execution inside container
async function executeInSandbox(
  ctx: SandboxContext,
  toolName: string,
  args: unknown,
  toolPolicies: ToolPolicy[]
): Promise<ToolResult> {
  // Check tool policy
  const policy = toolPolicies.find(p => p.tool === toolName);
  if (policy && !policy.allow) {
    return {
      content: `Tool "${toolName}" is not permitted in this sandbox`,
      isError: true,
      errorCode: 403,
    };
  }

  // Execute inside container
  const result = await exec("docker", [
    "exec", "-w", "/workspace",
    ctx.containerName,
    "openclaw-tool", toolName, JSON.stringify(args),
  ]);

  // Update activity timestamp
  ctx.lastActivityAt = new Date();

  return { content: result.stdout, isError: result.exitCode !== 0 };
}

Monitor containers for idle timeout and max age. A background loop runs every hour to clean up expired containers:

// sandbox/lifecycle.ts — idle and age monitoring
async function monitorContainers(
  containers: Map<string, SandboxContext>
): Promise<void> {
  const now = new Date();

  for (const [id, ctx] of containers) {
    const idleMs = now.getTime() - ctx.lastActivityAt.getTime();
    const ageMs = now.getTime() - ctx.createdAt.getTime();

    const idleLimitMs = DEFAULT_SANDBOX_IDLE_HOURS * 3600_000;
    const ageLimitMs = DEFAULT_SANDBOX_MAX_AGE_DAYS * 86400_000;

    if (idleMs > idleLimitMs || ageMs > ageLimitMs) {
      console.log(`Removing sandbox ${ctx.containerName}: ` +
        (idleMs > idleLimitMs ? "idle timeout" : "max age exceeded"));
      await exec("docker", ["rm", "-f", ctx.containerName]);
      containers.delete(id);
    }
  }
}
Resource efficiency: The lifecycle monitor prevents resource leaks from abandoned containers. The default 24-hour idle timeout is generous enough for interactive development sessions, while the 7-day max age ensures no container runs indefinitely.

Edge Cases & Error Handling

ScenarioBehaviorCode Location
Docker daemon not runningFallback to host execution with warning; or error if mode is "all" (strict)Sandbox resolver
Container startup timeoutContainer transitions to Failed; cleanup removes partially-created container; tool call retries on host if permitteddocker-backend.ts
Orphaned container (agent crash)Lifecycle monitor detects container exceeding max age and removes itlifecycle.ts
Workspace mount path doesn’t existDocker creates the directory automatically (default Docker bind-mount behavior)Container creation
Browser CDP port conflictContainer creation fails; fallback to random port or error if browser is requireddocker-backend.ts
SSH backend connection failureRetries with exponential backoff (3 attempts); then falls back to Docker if availablessh-backend.ts
Tool not in allow-listReturns ToolAuthorizationError (403) without executingexecutor.ts
Child agent with inherit mode + parent sandbox expiredChild detects stale context and creates a new sandboxsessions-spawn-tool.ts

Configuration

Config KeyDefaultDescription
sandbox.mode"non-main"When to sandbox: off, non-main (spawned agents only), all
sandbox.backend"docker"Isolation backend: docker or ssh
sandbox.workspaceRoot~/.openclaw/sandboxesBase directory for sandbox workspaces on host
sandbox.dockerImageopenclaw-sandbox:bookworm-slimDocker image for sandbox containers
sandbox.browser.cdpPort9222Chrome DevTools Protocol port for browser tools
sandbox.browser.autostartTimeoutMs12000Timeout waiting for browser to start inside container
sandbox.idleHours24Hours of inactivity before container is removed
sandbox.maxAgeDays7Maximum container age in days
sandbox.toolAllowSee DEFAULT_TOOL_ALLOWArray of tool names permitted inside the sandbox
sandbox.acpMode"inherit"ACP sandbox mode for child agents: inherit or require

Runtime Info

The sandbox-info.ts module provides a read-only view of the current sandbox state for debugging and monitoring:

// sandbox/sandbox-info.ts
export function getSandboxInfo(ctx: SandboxContext | null): SandboxInfo {
  if (!ctx) return { active: false };
  return {
    active: true,
    containerName: ctx.containerName,
    scope: ctx.scope,
    backend: ctx.dockerConfig ? "docker" : "ssh",
    mounts: ctx.workspaceMounts.map(m => ({
      host: m.hostPath,
      container: m.containerPath,
      mode: m.readOnly ? "ro" : "rw",
    })),
    browser: ctx.browserBridgeUrls ?? null,
    uptime: Date.now() - ctx.createdAt.getTime(),
    idleSince: Date.now() - ctx.lastActivityAt.getTime(),
  };
}

See also:

Directory Map

Source files for sandbox execution
src/agents/tools/
├── sandbox/
│   ├── types.ts                    ← SandboxConfig, SandboxContext, WorkspaceMount types
│   ├── defaults.ts                 ← Default constants (image, ports, timeouts, tool allow-list)
│   ├── config.ts                   ← Config builder with default fallbacks
│   ├── docker-backend.ts           ← Docker container creation and management
│   ├── ssh-backend.ts              ← SSH session creation and management
│   ├── executor.ts                 ← Tool execution inside sandbox (policy check + docker exec)
│   ├── lifecycle.ts                ← Idle/age monitoring and cleanup
│   └── sandbox-info.ts             ← Read-only runtime info for debugging
├── sessions-spawn-tool.ts          ← Agent spawning with ACP sandbox inheritance
└── tool-runtime.helpers.ts         ← Sandbox integration in tool runtime init