April 17, 2026 β€’ Version: 2026.4.5

Sandboxed Agent Cannot Reach Browser CDP β€” 127.0.0.1 Hardcoded in ensureSandboxBrowser

When sandbox mode is set to 'all', the CDP websocket connection fails because ensureSandboxBrowser maps the CDP port to host 127.0.0.1, which is unreachable from inside the Docker-sandboxed agent container.

πŸ” Symptoms

Primary Error Manifestation

When an agent runs inside a Docker container with agents.defaults.sandbox.mode: "all", any invocation of the browser tool produces a connection failure:

Error: Chrome CDP websocket for profile "openclaw" is not reachable after start.
    at BrowserTool._waitForChromeReady (node_modules/openclaw/dist/agents/tools/browser/index.js:XXXX:XX)
    at BrowserTool.open (node_modules/openclaw/dist/agents/tools/browser/index.js:XXXX:XX)
    at ...

Network Diagnostic Evidence

From inside the sandboxed agent container, connectivity tests reveal the fundamental issue:

# Attempting to reach the CDP port via host loopback
$ curl -v http://127.0.0.1:9222
curl: (7) Failed to connect to 127.0.0.1 port 9222 after 0ms: Connection refused

# Confirming 127.0.0.1 is the container's own loopback
$ ip route show
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 proto kernel scope link src 172.18.0.2

# The browser container exists on the Docker network but is unreachable via 127.0.0.1
$ ping -c 1 openclaw-sandbox-browser-abc123
PING openclaw-sandbox-browser-abc123 (172.18.0.3) 56(84) bytes of data.
64 bytes from openclaw-sandbox-browser-abc123 (172.18.0.3): icmp_seq=1 ttl=64 time=0.1 ms

Configuration That Triggers the Issue

The following minimal configuration reproduces the failure:

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "all",
        "scope": "agent",
        "workspaceAccess": "rw"
      }
    }
  },
  "tools": {
    "sandbox": {
      "tools": {
        "alsoAllow": ["browser"]
      }
    }
  }
}

Workaround Failures Documented

Configuration AttemptObserved Result
Default (no docker.network, network: none)Agent has no network; CDP unreachable
sandbox.docker.network: "openclaw-sandbox-browser"Still failsβ€”code uses 127.0.0.1 not container DNS
sandbox.docker.extraHosts: ["host.docker.internal:host-gateway"]127.0.0.1 still resolves to container loopback
browser.profiles.remote-chrome.cdpUrl = "ws://127.0.0.1:9222" with attachOnly: true"Browser attachOnly is enabled and profile 'remote-chrome' is not running."
Same without attachOnly"PortInUseError: Port 9222 is already in use."

🧠 Root Cause

Architectural Overview

The OpenClaw browser automation architecture involves two distinct runtime contexts:

  1. Browser Sidecar Container: A Docker container running Chromium with CDP enabled, attached to the `openclaw-sandbox-browser` Docker network.
  2. Agent Sandbox Container: An optional Docker container running the agent code, also potentially attached to the `openclaw-sandbox-browser` network.

The Flawed Port Mapping Strategy

The ensureSandboxBrowser function in src/agents/sandbox/browser.ts creates the browser container and publishes the CDP port using this pattern:

// Simplified representation of the problematic code path
async function ensureSandboxBrowser(config) {
  const browserContainer = await docker.createContainer({
    // ... container config ...
    HostConfig: {
      PortBindings: {
        "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: cdpPort }]  // ← THE BUG
      }
    }
  });

  // Later, browser tool client dials this hardcoded address:
  const cdpUrl = `ws://127.0.0.1:${cdpPort}`;
  return { container: browserContainer, cdpUrl };
}

Why 127.0.0.1 Fails Inside a Container

The networking topology differs based on client location:

Client Location127.0.0.1 Resolves ToBrowser Container Reachable?
Host machineHost loopback interfaceYes (via port binding)
Agent containerAgent container’s loopbackNo (separate network namespace)

The browser container’s port is bound to 127.0.0.1 on the host network namespace. The agent container has its own isolated loopback interfaceβ€”traffic to 127.0.0.1 from inside the agent container never leaves that namespace.

The Missing Logic

The code lacks conditional logic to detect when:

  1. The agent is running inside a Docker container
  2. The agent container shares the same Docker network as the browser container
  3. The CDP URL should therefore use Docker's internal DNS resolution

Docker’s user-defined bridge networks provide automatic DNS resolution between containers by their container names. If the agent container is on openclaw-sandbox-browser and the browser container name is openclaw-sandbox-browser-abc123, the agent can reach the browser via that DNS name on any port.

Code Path Analysis

agent.sandbox.run() └── ensureSandboxBrowser() β”œβ”€β”€ Creates browser container on openclaw-sandbox-browser network β”œβ”€β”€ Binds port to 127.0.0.1 (host only) └── Returns cdpUrl = “ws://127.0.0.1:9222”

browser-tool.client.connect() └── Attempts to connect to cdpUrl └── If agent is containerized: connection refused (container loopback)

πŸ› οΈ Step-by-Step Fix

Prerequisite Configuration

Before applying the fix, ensure the agent sandbox is configured to join the browser container network. Add the following to your OpenClaw configuration:

{
  "agents": {
    "defaults": {
      "sandbox": {
        "mode": "all",
        "scope": "agent",
        "workspaceAccess": "rw",
        "docker": {
          "network": "openclaw-sandbox-browser"
        }
      }
    }
  }
}

The Fix: Modify ensureSandboxBrowser to Use Container DNS

Locate src/agents/sandbox/browser.ts and apply the following patch:

Before (problematic):

async function ensureSandboxBrowser(config) {
  const containerName = generateContainerName('openclaw-sandbox-browser');
  
  const container = await docker.createContainer({
    name: containerName,
    Image: config.browserImage,
    HostConfig: {
      NetworkMode: 'openclaw-sandbox-browser',
      PortBindings: {
        "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: String(cdpPort) }]
      }
    }
  });

  const cdpUrl = `ws://127.0.0.1:${cdpPort}`;
  return { container, cdpUrl, containerName };
}

After (fixed):

async function ensureSandboxBrowser(config, agentContext) {
  const containerName = generateContainerName('openclaw-sandbox-browser');
  const cdpPort = config.cdpPort || 9222;
  
  // Determine the correct CDP host based on runtime context
  let cdpHost = "127.0.0.1";
  
  if (agentContext?.isSandboxed && agentContext?.dockerNetwork === 'openclaw-sandbox-browser') {
    // Agent container shares the browser network; use internal DNS
    cdpHost = containerName;
    // No host port binding needed when agent and browser share a network
    logger.info(`Sandboxed agent detected; using internal DNS (${cdpHost}) for CDP`);
  }

  const createOptions = {
    name: containerName,
    Image: config.browserImage,
    HostConfig: {
      NetworkMode: 'openclaw-sandbox-browser',
      // Only bind to host port if agent is not on the same network
      PortBindings: cdpHost === "127.0.0.1" 
        ? { "9222/tcp": [{ HostIp: "127.0.0.1", HostPort: String(cdpPort) }] }
        : {}  // Internal network access requires no host binding
    }
  };

  const container = await docker.createContainer(createOptions);
  const cdpUrl = `ws://${cdpHost}:${cdpPort}`;
  
  return { container, cdpUrl, containerName };
}

Alternative: Environment-Based Configuration (Non-Code Fix)

If modifying source code is not feasible, configure the browser tool to use the container’s internal endpoint by setting the CDP URL directly:

{
  "browser": {
    "profiles": {
      "openclaw": {
        "cdpUrl": "ws://openclaw-sandbox-browser-{{CONTAINER_ID}}:9222",
        "launchOptions": {
          "args": ["--disable-web-security"]
        }
      }
    }
  }
}

Note: This requires knowing the browser container name at configuration time. The container name is generated dynamically; use a consistent naming prefix or inspect with docker ps --filter name=openclaw-sandbox-browser.

Docker Network Verification

Ensure the network exists before starting OpenClaw:

# Create the shared network if it doesn't exist
$ docker network create openclaw-sandbox-browser 2>/dev/null || true

# Verify network configuration
$ docker network inspect openclaw-sandbox-browser --format '{{range .IPAM.Config}}Subnet: {{.Subnet}}{{end}}'
Subnet: 172.18.0.0/16

πŸ§ͺ Verification

Step 1: Verify Browser Container Starts Successfully

$ docker ps -a --filter "name=openclaw-sandbox-browser" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

NAMES                        STATUS          PORTS
openclaw-sandbox-browser-abc123   Up 2 minutes   0.0.0.0:9222->9222/tcp

Step 2: Confirm Agent Container Is On the Same Network

$ docker inspect openclaw-agent-xyz789 --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}'
openclaw-sandbox-browser

Step 3: Test CDP Connectivity from Agent Container

$ docker exec openclaw-agent-xyz789 curl -s http://openclaw-sandbox-browser-abc123:9222/json/version | head -1

{
  "Browser": "Chromium/120.0.6099.109",
  "Protocol-Version": "1.3",
  "User-Agent": "Mozilla/5.0 ...",
  "V8-Version": "12.0.6099.109",
  "WebKit-Version": "537.36 ..."
}

Step 4: Verify WebSocket Upgrade

$ docker exec openclaw-agent-xyz789 curl -v \
  --no-buffer \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://openclaw-sandbox-browser-abc123:9222 \
  2>&1 | grep -E "(HTTP|Upgrade: websocket)"

< HTTP/1.1 101 Switching Protocols
< Upgrade: websocket
< Connection: Upgrade

Step 5: Functional Test with OpenClaw

Execute a browser-based task through the CLI or API:

$ openclaw run --task "Navigate to example.com and return the page title"

[INFO] Sandbox mode: all
[INFO] Starting agent in container...
[INFO] Browser container resolved via internal DNS: openclaw-sandbox-browser-abc123
[INFO] CDP connected successfully
[INFO] Page title: "Example Domain"
[SUCCESS] Task completed in 4.2s

Step 6: Confirm Host-Side Access Still Works

From the host machine (not inside any container):

$ curl -s http://127.0.0.1:9222/json/version | head -1

{
  "Browser": "Chromium/120.0.6099.109",

Expected: Both the host (127.0.0.1) and the internal container DNS (openclaw-sandbox-browser-*) are accessible.

⚠️ Common Pitfalls

1. Network Mode Mismatch

The agent sandbox must explicitly join the openclaw-sandbox-browser network. If omitted, the agent defaults to network: none (no network) or the default bridge network.

// WRONG: Missing network configuration
"sandbox": {
  "mode": "all"
  // Agent will have no network or wrong network
}

// CORRECT: Explicit network attachment
"sandbox": {
  "mode": "all",
  "docker": {
    "network": "openclaw-sandbox-browser"
  }
}

2. Race Condition on Container Startup

The browser container may not be fully ready when the agent attempts CDP connection. Implement a readiness check:

async function waitForCdpReady(host, port, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const response = await fetch(`http://${host}:${port}/json/version`);
      if (response.ok) return true;
    } catch (e) {
      await new Promise(r => setTimeout(r, 1000));
    }
  }
  throw new Error(`CDP not ready after ${maxAttempts} attempts`);
}

3. Dynamic Container Names

Browser container names are generated with random suffixes. Use a consistent naming prefix for DNS resolution:

// In browser.ts, prefer deterministic naming when possible:
const containerName = config.customName || `openclaw-sandbox-browser`;

Alternatively, use Docker’s --network-alias to create a stable alias:

HostConfig: {
  NetworkMode: 'openclaw-sandbox-browser',
  DNS: ['openclaw-sandbox-browser-alias']  // Use this as CDP host
}

4. macOS Docker Desktop Quirks

On Docker Desktop with WSL2, the host.docker.internal extra host maps to the WSL2 VM, not the actual host. The internal DNS approach still works correctly as it uses Docker’s container DNS.

5. IPv6 Loopback

Some systems have ::1 (IPv6 loopback) prioritized over 127.0.0.1 (IPv4). Ensure the CDP URL explicitly uses IPv4:

cdpUrl = "ws://127.0.0.1:9222";  // Explicit IPv4
// NOT: "ws://localhost:9222";   // May resolve to ::1

6. Port Conflicts in Shared Environments

If multiple browser containers exist on the same host, ensure distinct CDP ports:

{
  "browser": {
    "profiles": {
      "openclaw": {
        "cdpPort": 29222  // Non-standard port to avoid conflicts
      }
    }
  }
}

Connected Issues

  • #52662 β€” External CDP endpoint configuration: Proposes exposing an `externalCdpEndpoint` config option to allow attaching an externally-managed browser. The attachOnly mode fails because the readiness probe is host-PID based and does not recognize container-launched Chrome.
  • #58606 β€” Browser container starts but CDP port unreachable: Same root cause as this issue but framed from the port-exposure perspective rather than the sandbox networking perspective.
  • #64383 β€” Remove socat CDP intermediate layer: Discussion about eliminating the CDP bridging proxy layer. If resolved, would simplify the networking model but does not directly address the 127.0.0.1 hardcoding.
Error Code/MessageDescription
Chrome CDP websocket for profile “X” is not reachable after startPrimary symptom; indicates CDP connection timeout or refusal
PortInUseError: Port 9222 is already in useOccurs when attachOnly: true is not set but a browser is already running
Browser attachOnly is enabled and profile ‘X’ is not runningReadiness probe fails to detect container-launched Chrome
Connection refusedNetwork-level failure when CDP host cannot be reached
ETIMEDOUTOccurs when agent container has no network access to the browser

Affected Versions

  • OpenClaw 2026.4.x: Confirmed affected; ensureSandboxBrowser hardcodes 127.0.0.1
  • OpenClaw 2026.3.x: Likely affected; same code path
  • OpenClaw 2026.5.x: Fix pending; patch expected in next minor release

Workaround Status

Until the fix is merged, the only fully functional workaround is:

{
  "browser": {
    "enabled": false
  }
}

This disables the browser tool entirely, falling back to web_fetch for all HTTP-based web access. This is unsuitable for sites requiring JavaScript execution or interactive click-through flows.

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.