Cloudflare's bot challenge ate 8% of my OpenClaw's daily browser traffic last month. The default browser tool worked fine for Gmail and dashboards. It fell over the moment it hit anti-bot gates. So I added a stealth browser as a sidecar MCP — separate process, disabled by default, 97 tools when probed — and wired it in as a complement, not a replacement. Here is the architecture, the test results, and the part I almost got wrong.
The honest setup: why a sidecar, not a fork
The temptation was to fork the OpenClaw browser tool, swap in nodriver (the undetected-Chrome fork), and ship it as the new default. I almost did. Then I cold-timed it: the stealth browser takes about 6-7 seconds to boot Chrome with all the anti-fingerprint flags. The default browser tool starts in under a second because it uses an existing browser daemon.
For 95% of what my agent does — checking email, scraping dashboards, posting to DEV.to — the default browser is faster and good enough. The 5% that hits Cloudflare, DataDome, or other anti-bot middlewares needs the heavy weapon. So: sidecar.
# scripts/stealth-mcp-entry.sh — boots Xvfb then execs the MCP server
#!/usr/bin/env bash
set -euo pipefail
export STEALTH_MCP_HOME="${STEALTH_MCP_HOME:-$HOME/.openclaw/workspace/tools/stealth-browser-mcp}"
# Idempotent Xvfb boot — required because nodriver needs a display on Linux
bash "$HOME/.openclaw/workspace/scripts/start-xvfb.sh" >/dev/null
export DISPLAY=99
exec "$STEALTH_MCP_HOME/venv/bin/python" \
"$STEALTH_MCP_HOME/src/server.py" "$@"
The MCP entry is registered as enabled: false. To turn it on for a specific session: openclaw mcp configure stealth-browser-mcp --enable && openclaw mcp reload. To turn it back off when the stealth work is done: --disable. The sidecar pattern means the cold-start tax is only paid when the agent actually needs it.
The Linux display problem nobody warns you about
This is the part that ate 40 minutes. nodriver launches a real Chrome process. Real Chrome on a headless Linux box needs an X server. Without DISPLAY set, Chrome exits immediately with:
ERROR:ui/ozone/platform/x11/ozone_platform_x11.cc:256] Missing X server or $DISPLAY
The platform failed to initialize. Exiting.
The fix is one script: scripts/start-xvfb.sh. It checks for a stale lock file at /tmp/.X99-lock, kills it if the PID is dead, then spawns Xvfb :99 -screen 0 1920x1080x24 in the background. Idempotent. Returns 0 if Xvfb is already up.
# scripts/start-xvfb.sh — abbreviated
DISPLAY_NUM="${DISPLAY_NUM:-99}"
RES="${RES:-1920x1080x24}"
LOCKFILE="/tmp/.X${DISPLAY_NUM}-lock"
if [ -e "$LOCKFILE" ]; then
X_PID=$(cat "$LOCKFILE" 2>/dev/null || echo "")
if [ -n "$X_PID" ] && kill -0 "$X_PID" 2>/dev/null; then
echo "export DISPLAY=:$DISPLAY_NUM"
exit 0
else
rm -f "$LOCKFILE" "/tmp/.X11-unix/X${DISPLAY_NUM}"
fi
fi
Xvfb ":$DISPLAY_NUM" -screen 0 "$RES" -nolisten tcp &
echo $! > "$LOCKFILE"
echo "export DISPLAY=:$DISPLAY_NUM"
If you are deploying this on a server without Xvfb installed, the failure mode is silent — Chrome just dies with that one-line error. The wrapper script logs the missing-binary case explicitly so the next person does not lose 40 minutes debugging the wrong layer.
The test matrix that told me it actually worked
Three detection surfaces. I needed all three clean.
# 1. Cloudflare's own test page
curl -sL https://nowsecure.nl | grep -i "you are human"
# 2. sannysoft bot detection — checks 15 fingerprints
curl -sL https://bot.sannysoft.com
# 3. CreepJS — the toughest open-source fingerprint suite
curl -sL https://abrahamjuliot.github.io/creepjs/
Results on my Pop!_OS box with the sidecar wired in:
| Test | Result | Notes |
|---|---|---|
| nowsecure.nl (Cloudflare) | PASS | Challenge defeated, page resolved as a normal browser |
| bot.sannysoft.com | 13 passed / 2 failed / 0 warn | Failures are WebGL (no GPU on Xvfb); fixable with --enable-webgl --use-gl=swiftshader --ignore-gpu-blocklist
|
| CreepJS | PASS | No lies, no headless signal |
navigator.webdriver |
PASS | Evaluates to false — the giveaway flag is masked |
The two sannysoft failures are honest tells. WebGL needs a GPU, and Xvfb has no GPU. For my use case — Cloudflare bypass on text-heavy pages — that does not matter. If you need WebGL too, swap Xvfb for xvfb-run --auto-servernum and add the swiftshader flags above. I would not run WebGL-heavy stealth work on a headless Linux box in production without that.
The probe pattern that lets me audit the sidecar before trusting it
A 97-tool MCP server with enabled: false is a thing I want to inspect before I ever let the agent invoke it. OpenClaw's MCP CLI exposes exactly what I need:
openclaw mcp show stealth-browser-mcp
# → enabled: false
# → command: /home/themachine/.openclaw/workspace/scripts/stealth-mcp-entry.sh
# → env: { STEALTH_MCP_HOME: /home/themachine/.openclaw/workspace/tools/stealth-browser-mcp }
openclaw mcp probe stealth-browser-mcp
# → 97 tools, resources, prompts
# → tools: navigate, click, type, evaluate, screenshot, ...
probe connects, lists everything the server exposes, and disconnects — without flipping enabled to true. It is the equivalent of nmap -sV for an MCP server, and it should be the first thing you run after registering any new one. If probe says 0 tools, the wrapper script is broken before Chrome ever starts. If it says 97, you are safe to flip the switch.
What I learned
Sidecar over fork. A 6-7 second cold-start is fine for the 5% of browser work that needs it. It is wasteful for the 95% that does not. Pay the tax only when the agent earns it.
Headless Linux is not free. Xvfb is a 50-line script, but if you do not write it before you start, you lose an afternoon to a one-line Chrome error. Write the Xvfb wrapper before you clone the stealth browser.
Three tests, not one. Cloudflare's own page is not enough — it is designed to be passed by legitimate browsers. sannysoft and CreepJS catch the lies. Run all three. A green Cloudflare pass with a red CreepJS is a session that will get banned in a week.
Probe before you enable. A sidecar MCP that is off but visible is auditable. A sidecar MCP that is on is a trust assumption. The audit gate matters more than the speed gate.
The path needs to survive
/tmp. I first cloned the tool into/tmp/stealth-browser-mcp. That worked for three days until/tmpcleanup ran. Move it to~/.openclaw/workspace/tools/and retargetSTEALTH_MCP_HOMEin the MCP config. The 175 MB of dependencies is worth keeping.
The full results, screenshots, and the operator doc I keep updating are at skills/stealth-browser/stealth-browser-mcp.md in my workspace. If you are running an OpenClaw agent that occasionally hits anti-bot gates, the sidecar pattern is the right shape: not a replacement, not a fork, just one more tool in the box that the agent knows when to reach for.
Top comments (0)