TL;DR — control a Chrome or Edge browser running on Windows from inside WSL2 (Linux) over the Chrome DevTools Protocol (CDP): navigate, click, type, screenshot, run JavaScript, and scrape page text/HTML.
🤖 AI agents: see AGENTS.md for a machine-readable invocation guide and llms.txt for a quick index.
Browser automation usually means spinning up an ephemeral, empty profile
(Playwright, Selenium). This instead attaches to a persistent Chrome/Edge
instance you launched with --remote-debugging-port=9222. Because the profile
persists across runs, logins and cookies you establish stay put — and you can
watch and hand-control the same window an agent is driving. Point it at your main
profile (close Chrome first) to drive your real session directly.
python3 chrome_cdp.py status
python3 chrome_cdp.py navigate "https://example.com"
python3 chrome_cdp.py screenshot /tmp/shot.pngWSL2 (Linux) Windows
──────────── ───────
chrome_cdp.py
│ resolves Windows host IP (from /etc/resolv.conf, or --host)
│ HTTP GET http://<host>:9222/json ──► Chrome/Edge (--remote-debugging-port=9222)
│ opens a WebSocket to the tab's │ persistent debug profile
│ webSocketDebuggerUrl │
│ sends CDP commands ──────────────────►│ Page.navigate, Runtime.evaluate,
│ receives results / screenshots ◄──────── Page.captureScreenshot, ...
▼
prints output / saves screenshot
- Chrome/Edge runs on Windows with
--remote-debugging-port=9222. - Tab management uses the HTTP
/json*endpoints; page interaction uses a WebSocket to the tab'swebSocketDebuggerUrl. - From WSL2 the tool resolves the Windows host IP from the
nameserverline in/etc/resolv.conf(override with--host/CHROME_CDP_HOST). - Both Chrome and Edge speak CDP (both are Chromium-based).
- Windows 10/11 with WSL2
- Chrome or Edge installed on Windows
- Python 3.7+ inside WSL with two packages:
pip install -r requirements.txt # requests, websocket-clientStart the browser with remote debugging enabled (the helper launches it on the Windows side via PowerShell):
bash scripts/start_chrome_cdp.shChrome's debug port only listens on the Windows localhost by default. If WSL2 can't reach it, add a one-time Windows portproxy (elevated shell) so the WSL-facing interface forwards to it:
netsh interface portproxy add v4tov6 listenport=9222 listenaddress=0.0.0.0 connectport=9222 connectaddress=::1Then verify from WSL:
python3 chrome_cdp.py statusGlobal flags --host, --port, --timeout go before the subcommand.
CDP="python3 chrome_cdp.py"
# Connection
$CDP status # check CDP connection (JSON)
# Tabs (HTTP)
$CDP list-tabs # list all open tabs
$CDP new-tab "https://example.com" # open a new tab
$CDP switch-tab <tab_id> # activate a tab
$CDP close-tab <tab_id> # close a tab
# Navigation
$CDP navigate "https://example.com" # open URL in a new tab
$CDP navigate "https://example.com" --tab-id X # navigate an existing tab
# Page info
$CDP title # page title
$CDP url # current URL
$CDP text # visible page text (innerText)
$CDP content # full page HTML (first 5000 chars)
$CDP screenshot /tmp/shot.png # save a PNG screenshot
# JavaScript
$CDP js "document.title"
$CDP js "[...document.querySelectorAll('.price')].map(e => e.textContent)"
# Interaction (CSS selectors)
$CDP click "#submit-button" # click an element
$CDP type "#search" "hello world" # set an input's value
$CDP wait ".results" # wait for an element (default 10s)
$CDP wait ".results" --wait-timeout 20 # custom timeout
# Override host/port (before the subcommand)
$CDP --host 127.0.0.1 --port 9222 statusMost WebSocket commands accept --tab-id <id> to target a specific tab
(otherwise the first page-type tab is used).
import sys
sys.path.insert(0, "/path/to/chrome-cdp-wsl")
from chrome_cdp import ChromeCDP
chrome = ChromeCDP() # or ChromeCDP(host=..., port=..., timeout=...)
chrome.navigate("https://example.com")
chrome.screenshot("/tmp/shot.png")
title = chrome.get_title()
links = chrome.execute_js("[...document.querySelectorAll('a')].map(a => a.href)")
chrome.click("#submit-button")
chrome.type_text("#search", "hello")
chrome.wait_for(".results", timeout=10)
chrome.close() # tear down the WebSocket when done| Variable | Default | Purpose |
|---|---|---|
CHROME_CDP_HOST |
nameserver in /etc/resolv.conf |
Windows host address |
CHROME_CDP_PORT |
9222 |
Remote-debugging port |
CHROME_PATH |
Chrome's default install path | Browser exe (start script) |
CHROME_DEBUG_PROFILE |
C:\chrome-debug-profile |
Debug profile dir (start script) |
- Connection failed — run
python3 chrome_cdp.py status. If not connected, the browser isn't running with debugging (scripts/start_chrome_cdp.sh) or WSL2 can't reach the port (add the portproxy above, or try--host 127.0.0.1under mirrored-mode WSL networking). - WebSocket 403 Forbidden — the browser needs
--remote-allow-origins=*(the start script includes it). - Element not found —
waitfor it beforeclick/type.
An open remote-debugging port is an unauthenticated control channel into the
browser session — anything that can reach port 9222 can act as that browser.
- Only enable it on a machine you control; keep the port on the local WSL↔Windows link, not the wider LAN.
- The
0.0.0.0portproxy above exposes the port on all interfaces — restrict it with the firewall if your machine isn't on a trusted network. - Treat
jslike any code-execution path: don't pass it untrusted input.
Chrome · Edge · Chromium · Chrome DevTools Protocol · CDP · WSL · WSL2 ·
browser automation · headful · remote debugging · --remote-debugging-port ·
persistent profile · logged-in session · WebSocket · screenshot · navigate ·
scrape · Playwright alternative · Linux-to-Windows
MIT — see LICENSE.