OpenClaw's headless nodes couldn't run shell commands. So I built a socat tunnel chain that gives me full browser control — navigate, screenshot, DOM snapshot — across 5 nodes in 3.7 seconds.
🇹🇭 เจอบัคที่สั่ง run คำสั่งไม่ได้ เลย bypass ด้วย socat ได้ browser control เต็มรูปแบบ 5 เครื่องใน 3.7 วินาที
OpenClaw fleet v2 was live: 5 KVM nodes, 5 Chromium browsers, all connected to the gateway via WireGuard. The dashboard showed 5/5 green. Commands like system.which returned instantly.
But anything that needed exec approval — system.run, nodes run, any shell command — timed out at exactly 120 seconds. Every single time.
$ openclaw nodes run --node openclaw-node-1 --raw "hostname"
... 120 seconds later ...
Error: exec.approval.request timeout 120007ms
After three debugging sessions, SSH'ing into nodes, reading logs, restarting services, switching from system to user systemd services — I found the root cause:
$ ls ~/.openclaw/exec-approvals.sock
ls: cannot access '.../exec-approvals.sock': No such file or directoryThe file exec-approvals.json existed (with a */* wildcard allowlist). But the Unix socket exec-approvals.sock — which the node host process needs to create for IPC — was never created. The node host process on Linux headless simply doesn't create it.
The flow: nodes run → gateway → node → check approval socket → socket doesn't exist → wait for IPC response → timeout 120s.
My theory: the approval socket is designed for the macOS companion app (which has a GUI for approval prompts). The headless Linux node host was never updated to create it automatically.
Before giving up on OpenClaw's invoke system, I mapped every browser.proxy path systematically:
# Tested every path from the openclaw browser CLI
for path in / /tabs /snapshot /status /navigate /screenshot /click /open ...; do
openclaw nodes invoke --node openclaw-node-1 \
--command browser.proxy \
--params "{\"path\":\"$path\"}" --json
doneResults:
| Path | Status | Returns |
|---|---|---|
/ |
OK | Browser status (running, cdpReady, version) |
/tabs |
OK | Open tabs with titles, URLs, WebSocket URLs |
/snapshot |
OK | Full DOM tree in AI-readable format |
/console |
OK | Recent console messages |
/errors |
OK | Page errors |
/requests |
OK | Recent network requests |
/navigate |
Not Found | — |
/screenshot |
Not Found | — |
/click |
Not Found | — |
/open |
Not Found | — |
| Everything else | Not Found | — |
browser.proxy is read-only. Despite the name "proxy", it only exposes 6 monitoring endpoints. No write operations. No navigation. No screenshots.
The key insight: each node has a perfectly working Chromium with CDP (Chrome DevTools Protocol) on localhost:18792. The only problem is routing — the gateway can't reach it.
Two hops of socat solve this:
Each node's Chromium binds to 127.0.0.1:18792 (localhost only). A socat forward exposes it:
# On each node (or as systemd service):
socat TCP-LISTEN:18793,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:18792Now 10.10.0.11:18793 (the node's WireGuard IP) reaches Chromium.
# On gateway — one per node:
socat TCP-LISTEN:18801,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.11:18793 # node-1
socat TCP-LISTEN:18802,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.12:18793 # node-2
socat TCP-LISTEN:18803,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.13:18793 # node-3
socat TCP-LISTEN:18804,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.14:18793 # node-4
socat TCP-LISTEN:18805,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.15:18793 # node-5The gateway's openclaw browser CLI uses cdpUrl: http://localhost:18801. It thinks it's talking to a local Chromium. It's actually talking to node-1's Chromium — through two socat hops and a WireGuard tunnel:
openclaw browser navigate "https://example.com"
→ localhost:18801 (gateway socat)
→ 10.10.0.11:18793 (WireGuard tunnel)
→ 127.0.0.1:18792 (node-1 Chromium CDP)
→ Chrome navigates to example.com
→ {"ok": true, "url": "https://example.com/"}
# fleet-demo.py (runs on gateway)
sites = [
("node-1", 18801, "https://news.ycombinator.com"),
("node-2", 18802, "https://github.com/trending"),
("node-3", 18803, "https://www.bbc.com/news"),
("node-4", 18804, "https://en.wikipedia.org/wiki/Main_Page"),
("node-5", 18805, "https://httpbin.org/ip"),
]
# Connect to each node's CDP WebSocket, navigate, screenshot — all in parallel
results = await asyncio.gather(*[demo_node(n, p, u) for n, p, u in sites])Output:
OpenClaw Fleet Browser Demo
Controlling 5 nodes via Chrome DevTools Protocol
node-1: "Hacker News" (94KB screenshot)
node-2: "GitHub Trending" (77KB screenshot)
node-3: "BBC News" (174KB screenshot)
node-4: "Wikipedia" (101KB screenshot)
node-5: "DuckDuckGo" (138KB screenshot)
5/5 nodes OK — completed in 3.7s
Five different websites. Five screenshots. Five DOM snapshots. All captured in parallel via CDP WebSocket connections through socat tunnels over WireGuard.
# SSH tunnel to gateway
ssh -f -N -L 18789:localhost:18789 root@152.42.206.137
# Navigate node-1's browser
openclaw browser navigate "https://example.com" \
--url ws://localhost:18789 --token $TOKEN
# Take a screenshot
openclaw browser screenshot \
--url ws://localhost:18789 --token $TOKEN
# Query any node's tabs directly
openclaw nodes invoke --node openclaw-node-3 \
--command browser.proxy --params '{"path":"/tabs"}' \
--url ws://localhost:18789 --token $TOKENOpen http://localhost:18789/dashboard/chat — type natural language commands. The AI agent (GLM-5) takes screenshots, navigates, and describes what it sees.
openclaw browser navigate "https://example.com"
openclaw browser screenshot
openclaw browser snapshotAd-hoc socat dies on reboot. Systemd services don't:
[Unit]
Description=CDP socat forwarder (listen :%i -> localhost:18792)
After=chromium-cdp.service
Wants=chromium-cdp.service
[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:%i,fork,reuseaddr,bind=0.0.0.0 TCP:127.0.0.1:18792
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.targetsystemctl enable --now cdp-socat-node@18793.service# cdp-socat-gw-node1.service, cdp-socat-gw-node2.service, etc.
ExecStart=/usr/bin/socat TCP-LISTEN:18801,fork,reuseaddr,bind=127.0.0.1 TCP:10.10.0.11:18793MBA (macOS)
│
│ SSH tunnel (port 18789)
│
▼
DO Gateway (152.42.206.137)
│ OpenClaw Gateway (ws://localhost:18789)
│
│ cdpUrl → localhost:18801
│
├── socat :18801 ──► WG 10.10.0.11:18793 ──► node-1 Chrome :18792
├── socat :18802 ──► WG 10.10.0.12:18793 ──► node-2 Chrome :18792
├── socat :18803 ──► WG 10.10.0.13:18793 ──► node-3 Chrome :18792
├── socat :18804 ──► WG 10.10.0.14:18793 ──► node-4 Chrome :18792
└── socat :18805 ──► WG 10.10.0.15:18793 ──► node-5 Chrome :18792
│
5x KVM VMs on black.local
Ubuntu 24.04, Chromium 145
Headless + Xvfb + noVNC
- Chrome 145 snap rejects
Host: 127.0.0.1— Always uselocalhostincdpUrl, not127.0.0.1 - Gateway caches CDP connections — Changing
cdpUrlin config file requires gateway restart. No runtime switching. - browser.proxy is read-only — Don't waste time trying write paths. Use direct CDP for full control.
- WebSocket URL port mismatch — CDP's
/json/listreturnsws://localhost:18792/devtools/page/...but you need to rewrite the port to match your tunnel port.
| Component | Monthly Cost |
|---|---|
| DO Gateway | $6 (s-1vcpu-2gb) |
| 5 KVM VMs | $0 (home server) |
| WireGuard | $0 |
| Socat | $0 |
| OpenClaw | $0 (open source) |
| Total | $6/month |
When software has a broken internal routing system, don't spend days debugging the internals. Step back, look at what endpoints actually work (Chromium's CDP was always healthy), and bridge them externally with the simplest tool available.
Two lines of socat. That's all it took.
Part of the OpenClaw Fleet v2 series. Built with KVM, WireGuard, socat, and stubbornness. By Nat Weerawan + Homekeeper Oracle — 2026-02-23