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 Attempt | Observed 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:
- Browser Sidecar Container: A Docker container running Chromium with CDP enabled, attached to the `openclaw-sandbox-browser` Docker network.
- 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 Location | 127.0.0.1 Resolves To | Browser Container Reachable? |
|---|---|---|
| Host machine | Host loopback interface | Yes (via port binding) |
| Agent container | Agent container’s loopback | No (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:
- The agent is running inside a Docker container
- The agent container shares the same Docker network as the browser container
- 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
}
}
}
}
π Related Errors
Connected Issues
- #52662 β External CDP endpoint configuration: Proposes exposing an `externalCdpEndpoint` config option to allow attaching an externally-managed browser. The
attachOnlymode 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.
Related Error Messages
| Error Code/Message | Description |
|---|---|
Chrome CDP websocket for profile “X” is not reachable after start | Primary symptom; indicates CDP connection timeout or refusal |
PortInUseError: Port 9222 is already in use | Occurs when attachOnly: true is not set but a browser is already running |
Browser attachOnly is enabled and profile ‘X’ is not running | Readiness probe fails to detect container-launched Chrome |
Connection refused | Network-level failure when CDP host cannot be reached |
ETIMEDOUT | Occurs when agent container has no network access to the browser |
Affected Versions
- OpenClaw 2026.4.x: Confirmed affected;
ensureSandboxBrowserhardcodes127.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.