Claude in Chrome doesn't work on WSL2. Here's the fix: one logged-in Chrome, two drivers, cheap tokens A developer solved the problem of Claude Code in WSL2 not working with the Claude in Chrome extension by using the Chrome DevTools Protocol (CDP) instead. The solution involves running a single Chrome instance on Windows with remote debugging enabled, then attaching two CDP clients from WSL2: one for cheap accessibility tree snapshots and another for network/console/Lighthouse access. This setup avoids expensive screenshot-based interactions and maintains a persistent logged-in session. Claude Code can drive Chrome natively through the Claude in Chrome extension, and it will not do it from WSL2. The reason is structural rather than a missing flag. The extension runs inside Windows Chrome, which discovers native messaging hosts through the Windows registry, while Claude Code running in WSL installs its host manifest into the Linux filesystem at ~/.config/google-chrome/NativeMessagingHosts/ , pointing at a Linux binary. Neither half can see the other. On my machine the Linux manifest is sitting there and the Windows registry key simply doesn't exist, so /chrome reports "Extension not detected" and no amount of reinstalling changes that. The docs say WSL is unsupported. There are open issues that suggest this gets fixed eventually, and there's an unofficial Windows-side bridge if you enjoy that kind of thing, but I wanted something that worked now. So you fall back to the Chrome DevTools Protocol, which works, and which is where every guide I found stops: you can now take a screenshot. Screenshots are the expensive part. A single page costs a few thousand tokens as an image, and an agent working through a ten step form takes one at every step. The thing it actually needs, which elements exist and which ones it can click, comes to a few hundred tokens if you ask for the accessibility tree instead. The other half of the problem is login. A fresh Chrome for Testing has never seen your app's sign-in screen, so if your dev app puts everything behind auth, an agent driving that browser spends its life staring at a login form. You want the browser you already logged into, and you want to drive it with something cheaper than pictures. That's the setup below: one Chrome on 9222, two drivers attached to it, and a rule for which one to use when. Windows chrome.exe --remote-debugging-port=9222 CDP server, HTTP + WebSocket --user-data-dir=...\ChromeDebugProfiles\myapp isolated, stays logged in bound to 127.0.0.1 only localhost:9222 shared loopback via networkingMode=Mirrored WSL2 agent-browser attaches by CDP cheap a11y snapshots, default driver chrome-devtools-mcp attaches by CDP network, console, Lighthouse Claude Code drives both One browser, two clients on it. CDP is a plain HTTP and WebSocket server and it accepts multiple simultaneous clients, so this isn't a trick anyone should feel clever about. The one rule is that each client gets its own tab, since two clients driving the same active page will conflict. You need networkingMode=Mirrored in your Windows .wslconfig , which is the thing that makes localhost:9222 mean the same address on both sides of the WSL boundary, and you need a Chrome listening on 9222 with an isolated profile. If you don't have that yet, Nebrass Lamouchi's post https://blog.nebrass.fr/playing-with-wsl2-and-chrome-devtools-mcp/ covers the setup properly and I'm not going to re-teach it here. One warning from that side: mirrored mode can fight with Docker Desktop, and running Docker Engine natively in WSL2 was easier than trying to make them coexist. The minimum launcher, from Windows or from WSL through /mnt/c : "/mnt/c/Program Files/Google/Chrome/Application/chrome.exe" \ --remote-debugging-port=9222 \ --user-data-dir="C:\Users\youruser\AppData\Local\ChromeDebugProfiles\myapp" \ --no-first-run \ --no-default-browser-check \ --remote-allow-origins=' ' & The --user-data-dir is doing more work than it looks like. It gives you a profile separate from your everyday Chrome, so the debug browser never touches your real cookies or extensions, and it persists across runs, so you sign into your app once inside it and the session is still there tomorrow. That persistence is most of why this is worth setting up at all. Confirm it's alive before connecting anything: curl -s http://127.0.0.1:9222/json/version | python3 -m json.tool A healthy response has "Browser": "Chrome/..." and a webSocketDebuggerUrl in it. If you get nothing back, skip ahead to the gotchas, because one of them is probably why. vercel-labs/agent-browser is a browser automation CLI aimed at AI agents, Apache-2.0, out of Vercel Labs. I looked at provenance before installing because the package ships a native binary: the npm package does map to the GitHub repo, it's published through GitHub Actions with OIDC and SLSA provenance, and it has zero runtime dependencies with a clean audit. The postinstall pulls a prebuilt Rust binary from GitHub releases over HTTPS without verifying a checksum, which is the part I'm least comfortable with. It's standard practice for native-binary npm packages and I installed it anyway, so take that for whatever it's worth. npm install -g agent-browser The docs will point you at agent-browser install , which downloads Chrome for Testing. Skip it. That's the logged-out browser this whole post is trying to avoid. First run under WSL2 fails like this: ✗ Failed to create socket directory: Permission denied agent-browser runs a session daemon over a Unix socket under $XDG RUNTIME DIR , and on WSL2 /run/user/1000 usually doesn't exist. Point it at somewhere writable: export XDG RUNTIME DIR=/tmp/abr-runtime mkdir -p "$XDG RUNTIME DIR" && chmod 700 "$XDG RUNTIME DIR" The export doesn't survive between separate shell invocations, which matters when an agent is calling the CLI one command at a time and each call gets a fresh shell. Putting it in ~/.bashrc with the mkdir clears that up. agent-browser connect 9222 connect 9222 attaches over CDP to the Chrome you already have running, with your login already in it. If you let agent-browser start its own browser instead, you get a fresh Linux Chrome for Testing that's logged out and wants a display or headless mode, which throws away the only property here worth having. There's also --auto-connect , which discovers a running Chrome for you, though I'd rather name the port than trust discovery. Ask what's on the page, get back a tree of elements with refs, act on the refs. agent-browser snapshot textbox "code" ref=e7 textbox "message" ref=e17 button "Send" ref=e18 agent-browser fill @e7 "ABC-123" agent-browser type @e17 "hello, can you help with this?" agent-browser click @e18 agent-browser get text "main" get text reads the result back as plain text for almost nothing. Screenshot when you genuinely need pixels, like a visual regression or a layout bug, rather than to check whether a click landed. Two things caught me out. Refs go stale as soon as the DOM changes, so take a fresh snapshot after anything that re-renders instead of reusing e17 from thirty seconds ago. And for single page apps, pushstate