Skip to main content
The tensorlake/ubuntu-vnc image ships with Google Chrome pre-installed. Combined with Local Tunnels, this gives you a real, sandboxed Chrome that any DevTools-Protocol client (Playwright, Puppeteer, chrome-remote-interface, plain WebSocket) can drive from your laptop as if it were running locally — no headless container, no screenshot polling, no public port. If you want to drive the whole XFCE desktop (mouse, keyboard, screenshots) instead of just Chrome, use the higher-level Computer Use API — it talks to the same tensorlake/ubuntu-vnc image through connect_desktop() / connectDesktop(). The two workflows compose: keep the agent loop on CDP and attach a human reviewer over VNC. This guide walks through:
  1. Launching tensorlake/ubuntu-vnc.
  2. Starting Chrome with CDP enabled on the desktop session.
  3. Tunneling the CDP port to 127.0.0.1.
  4. Driving the browser from Python or Playwright.

Prerequisites

You can also use tl login to obtain a Personal Access Token interactively. The desktop password for the managed tensorlake/ubuntu-vnc image is tensorlake.

1. Launch the Sandbox

chrome-cdp is a name (optional, but it lets you suspend and resume later). The CLI prints the new sandbox id; reuse it as <sandbox-id> below. Four CPUs and 4 GiB of RAM is a comfortable default for a single Chrome session.

2. Start Chrome with CDP Enabled

Start Chrome on the existing VNC display (:1) as the desktop user (tl-user). Two flags matter:
  • --remote-debugging-port=9222 opens the DevTools Protocol endpoint on 127.0.0.1:9222 inside the sandbox.
  • --remote-allow-origins=* is required by current Chrome versions before they will accept a WebSocket whose Origin is anything other than the request host. Without it the HTTP /json/version endpoint works but ws://127.0.0.1:9222/devtools/... returns 403 Forbidden.
Confirm CDP is up:
You should see a JSON response with Browser, Protocol-Version, and webSocketDebuggerUrl.
Because Chrome is running on the VNC display :1, you can also attach a VNC viewer through the Local Tunnels workflow and watch it operate in real time. CDP control and human observation can run side by side.

3. Open a Tunnel

Forward 127.0.0.1:9222 on your laptop to 127.0.0.1:9222 inside the sandbox:
Leave the command running. Open a second terminal for the rest of this guide.
Verify locally:
Same JSON, but reached from your laptop. Every byte transits an authenticated WebSocket — port 9222 never has to be in exposed_ports.

4. Drive the Browser

Open a Tab

CDP exposes an HTTP control surface on the same port. Open a fresh tab with a PUT:
The response includes a webSocketDebuggerUrl for the new tab. List all tabs with curl http://127.0.0.1:9222/json/list and close one with curl http://127.0.0.1:9222/json/close/<target-id>.

Playwright

Raw CDP via WebSocket

When you want to issue protocol calls directly — Runtime.evaluate, Page.navigate, DOM.getDocument — connect to the per-tab WebSocket and exchange JSON messages:
This is also the path you take when wiring CDP into an LLM agent: expose open_url, evaluate, and list_targets as tools that wrap these calls.

Coding Agents (chrome-devtools MCP)

Claude Code and OpenAI Codex can both drive the same sandboxed Chrome through the official chrome-devtools-mcp server. The MCP attaches to an existing Chrome via --browser-url; match that URL to the tunnel’s local port and no other configuration is needed — using Chrome’s canonical 9222 on both sides keeps everything default-on-default. Register the MCP once for your user:
Stored at user scope by default. Pass --scope project to write it to the current project’s .mcp.json instead.
The --browser-url flag is what tells the MCP to attach to an existing Chrome instead of launching its own. With Chrome already running inside the sandbox (step 2) and a tunnel open at the default local port:
restart the agent so it picks up the new MCP (Claude Code re-reads on launch; Codex reads config.toml at startup and does not hot-reload), then ask it to do something in the browser:
The agent routes that through chrome-devtools127.0.0.1:9222 → tunnel → sandbox Chrome on display :1. If port 9222 is already taken on your laptop (a local Chrome with debugging on, another tunnel, etc.), pick any free port for both sides and keep them aligned:
Verify the path before you point an agent at it: curl http://127.0.0.1:9222/json/version should return Chrome’s JSON. The tunnel CLI keeps the local port bound even when the sandbox upstream goes away (terminated, suspended without auto-resume), so a hung curl usually means the sandbox is gone, not that the MCP is misconfigured.

5. Tear Down

Stop the tunnel with Ctrl+C. Stop Chrome inside the sandbox when you no longer need it:
Suspend the sandbox to keep the user-data-dir warm for next time, or terminate it to release resources:

Notes and Pitfalls

  • --remote-allow-origins=* is required for Chrome ≥ 111. Without it, the HTTP CDP endpoints work but every WebSocket handshake fails with 403. Restart Chrome with the flag if you forget.
  • Bind address. --remote-debugging-port only listens on 127.0.0.1 by default, which is exactly what you want — the tunnel forwards to 127.0.0.1 inside the sandbox, so DevTools stays unreachable from anywhere else.
  • --user-data-dir is required for CDP. Chrome ≥ 136 refuses to enable --remote-debugging-port against the default profile and prints DevTools remote debugging requires a non-default data directory. Specify this using --user-data-dir. to its log. Always pass --user-data-dir=/tmp/<something> (or any path other than ~/.config/google-chrome).
  • Headless mode. If you do not need the VNC view, you can launch with --headless=new instead of attaching to display :1. The tunneling and CDP usage remain identical.
  • Sandboxing inside containers. Chrome’s setuid sandbox sometimes fails inside container/VM combinations. If you see Failed to move to new namespace errors, add --no-sandbox to the launch flags.
  • Multiple agents. Each tab has its own webSocketDebuggerUrl. Two clients can drive different tabs of the same Chrome at the same time — useful when an agent loop and a human reviewer both want a window.
  • Computer Use — drive the full XFCE desktop (mouse, keyboard, screenshots) on the same tensorlake/ubuntu-vnc image.
  • Local Tunnels — the tunneling primitive that carries CDP traffic from your laptop into the sandbox.
  • Snapshots — fork a warmed-up Chrome profile so parallel agents start with cookies, history, and extensions already in place.