<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: karthik rathod</title>
    <description>The latest articles on DEV Community by karthik rathod (@karthik_rathod_592ae48161).</description>
    <link>https://dev.to/karthik_rathod_592ae48161</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2963376%2F0e1c31bb-782a-4ebb-8a3c-b3a1179646a9.jpg</url>
      <title>DEV Community: karthik rathod</title>
      <link>https://dev.to/karthik_rathod_592ae48161</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karthik_rathod_592ae48161"/>
    <language>en</language>
    <item>
      <title>Claude in Chrome doesn't work on WSL2. Here's the fix: one logged-in Chrome, two drivers, cheap tokens</title>
      <dc:creator>karthik rathod</dc:creator>
      <pubDate>Thu, 16 Jul 2026 13:23:56 +0000</pubDate>
      <link>https://dev.to/karthik_rathod_592ae48161/claude-in-chrome-doesnt-work-on-wsl2-heres-the-fix-one-logged-in-chrome-two-drivers-cheap-5ke</link>
      <guid>https://dev.to/karthik_rathod_592ae48161/claude-in-chrome-doesnt-work-on-wsl2-heres-the-fix-one-logged-in-chrome-two-drivers-cheap-5ke</guid>
      <description>&lt;p&gt;Claude Code can drive Chrome natively through the Claude in Chrome extension. Run &lt;code&gt;/chrome&lt;/code&gt; inside WSL2 and you get this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude in Chrome is not supported in WSL at this time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's on 2.1.211. It isn't a detection failure or a misconfiguration you can flag your way out of. Claude Code checks whether it's running under WSL and declines before it tries anything, so there's no connection to debug and no extension to reinstall.&lt;/p&gt;

&lt;p&gt;It's worth understanding why the gate is there, because it explains why the workarounds floating around don't work either. Chrome finds native messaging hosts through the Windows registry. Go looking, and there is a key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HKCU\Software\Google\Chrome\NativeMessagingHosts\com.anthropic.claude_browser_extension
    -&amp;gt; C:\Users\youruser\AppData\Roaming\Claude\ChromeNativeHost\...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which looks like success until you read it twice. That host belongs to the Claude desktop app on Windows. The one Claude Code installs is called &lt;code&gt;com.anthropic.claude_code_browser_extension&lt;/code&gt;, note the &lt;code&gt;_code_&lt;/code&gt; in the middle, and on WSL2 it goes here instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;~/.config/google-chrome/NativeMessagingHosts/com.anthropic.claude_code_browser_extension.json&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/home/youruser/.claude/chrome/chrome-native-host&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="err"&gt;(a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Linux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;binary)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two different hosts, two different filesystems. Even if the WSL check disappeared tomorrow, Windows Chrome would enumerate the registry and never find Claude Code's manifest, because the manifest is sitting in the Linux filesystem pointing at a Linux binary. It's in the right place for the wrong operating system, and reinstalling puts it right back there.&lt;/p&gt;

&lt;p&gt;Which brings me to the muddle that cost me an hour. The docs say WSL isn't supported and the CLI agrees. But the changelog for 2.1.0 says "Fixed Claude in Chrome support for WSL environments," and both statements have been live for months. There's an open docs issue about exactly that contradiction (&lt;a href="https://github.com/anthropics/claude-code/issues/18061" rel="noopener noreferrer"&gt;anthropics/claude-code#18061&lt;/a&gt;), still unresolved, where people keep arriving with the same confusion.&lt;/p&gt;

&lt;p&gt;That thread also carries a suggested workaround, &lt;code&gt;CLAUDE_CODE_PROXY_RESOLVES_HOSTS=true&lt;/code&gt; plus mirrored networking, which one commenter reports working and another reports doing nothing. I went and read what that flag actually does in the bundle. It's declared alongside &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, &lt;code&gt;NO_PROXY&lt;/code&gt;, and the cert settings, and it controls whether your HTTP proxy resolves DNS names instead of your client resolving them locally, the same distinction as &lt;code&gt;socks5&lt;/code&gt; versus &lt;code&gt;socks5h&lt;/code&gt;. Useful if a corporate proxy is between you and the API. It has nothing to do with native messaging, which isn't even a network protocol, and it certainly can't talk its way past a WSL check. My guess is the thread has two different bugs in it and the flag fixed the other one.&lt;/p&gt;

&lt;p&gt;The fallback is the Chrome DevTools Protocol, which works fine, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this assumes
&lt;/h2&gt;

&lt;p&gt;You need &lt;code&gt;networkingMode=Mirrored&lt;/code&gt; in your Windows &lt;code&gt;.wslconfig&lt;/code&gt;, which is the thing that makes &lt;code&gt;localhost:9222&lt;/code&gt; 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, &lt;a href="https://blog.nebrass.fr/playing-with-wsl2-and-chrome-devtools-mcp/" rel="noopener noreferrer"&gt;Nebrass Lamouchi's post&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;The minimum launcher, from Windows or from WSL through &lt;code&gt;/mnt/c&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="s2"&gt;"/mnt/c/Program Files/Google/Chrome/Application/chrome.exe"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remote-debugging-port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;9222 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--user-data-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"C:&lt;/span&gt;&lt;span class="se"&gt;\U&lt;/span&gt;&lt;span class="s2"&gt;sers&lt;/span&gt;&lt;span class="se"&gt;\y&lt;/span&gt;&lt;span class="s2"&gt;ouruser&lt;/span&gt;&lt;span class="se"&gt;\A&lt;/span&gt;&lt;span class="s2"&gt;ppData&lt;/span&gt;&lt;span class="se"&gt;\L&lt;/span&gt;&lt;span class="s2"&gt;ocal&lt;/span&gt;&lt;span class="se"&gt;\C&lt;/span&gt;&lt;span class="s2"&gt;hromeDebugProfiles&lt;/span&gt;&lt;span class="se"&gt;\m&lt;/span&gt;&lt;span class="s2"&gt;yapp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-first-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-default-browser-check&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--remote-allow-origins&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'*'&lt;/span&gt; &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--user-data-dir&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Confirm it's alive before connecting anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9222/json/version | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A healthy response has &lt;code&gt;"Browser": "Chrome/..."&lt;/code&gt; and a &lt;code&gt;webSocketDebuggerUrl&lt;/code&gt; in it. If you get nothing back, skip ahead to the gotchas, because one of them is probably why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheap driver: agent-browser
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vercel-labs/agent-browser&lt;/code&gt; 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 &lt;code&gt;postinstall&lt;/code&gt; 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; agent-browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docs will point you at &lt;code&gt;agent-browser install&lt;/code&gt;, which downloads Chrome for Testing. Skip it. That's the logged-out browser this whole post is trying to avoid.&lt;/p&gt;

&lt;h3&gt;
  
  
  The socket fix
&lt;/h3&gt;

&lt;p&gt;First run under WSL2 fails like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;✗ Failed to create socket directory: Permission denied
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;agent-browser runs a session daemon over a Unix socket under &lt;code&gt;$XDG_RUNTIME_DIR&lt;/code&gt;, and on WSL2 &lt;code&gt;/run/user/1000&lt;/code&gt; usually doesn't exist. Point it at somewhere writable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/abr-runtime
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;700 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;~/.bashrc&lt;/code&gt; with the mkdir clears that up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attach, never launch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser connect 9222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connect 9222&lt;/code&gt; 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 &lt;code&gt;--auto-connect&lt;/code&gt;, which discovers a running Chrome for you, though I'd rather name the port than trust discovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snapshot, then act on refs
&lt;/h3&gt;

&lt;p&gt;Ask what's on the page, get back a tree of elements with refs, act on the refs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agent-browser snapshot
&lt;span class="c"&gt;# textbox "code" [ref=e7]&lt;/span&gt;
&lt;span class="c"&gt;# textbox "message" [ref=e17]&lt;/span&gt;
&lt;span class="c"&gt;# button "Send" [ref=e18]&lt;/span&gt;

agent-browser fill @e7 &lt;span class="s2"&gt;"ABC-123"&lt;/span&gt;
agent-browser &lt;span class="nb"&gt;type&lt;/span&gt; @e17 &lt;span class="s2"&gt;"hello, can you help with this?"&lt;/span&gt;
agent-browser click @e18
agent-browser get text &lt;span class="s2"&gt;"main"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get text&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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, &lt;code&gt;pushstate &amp;lt;url&amp;gt;&lt;/code&gt; is the navigation command you want, not &lt;code&gt;open&lt;/code&gt;. I lost a chunk of an evening to that one: &lt;code&gt;open&lt;/code&gt; waits on a load event that an SPA route change never fires, so it reports a timeout on a page that has in fact loaded and is sitting right there in front of you. &lt;code&gt;pushstate&lt;/code&gt; detects the router, Next.js included, and goes through it.&lt;/p&gt;

&lt;p&gt;The rest of the surface is roughly what you'd guess. &lt;code&gt;find role|text|label|placeholder|testid &amp;lt;value&amp;gt; &amp;lt;action&amp;gt;&lt;/code&gt; for locating things, &lt;code&gt;get url|title|text|html|value&lt;/code&gt; and &lt;code&gt;is visible|enabled|checked&lt;/code&gt; for state, &lt;code&gt;console&lt;/code&gt; / &lt;code&gt;errors&lt;/code&gt; / &lt;code&gt;network requests&lt;/code&gt; / &lt;code&gt;vitals&lt;/code&gt; for a quick look, &lt;code&gt;tab new|list|close&lt;/code&gt; for tabs. One wrinkle worth knowing before it confuses you: &lt;code&gt;type&lt;/code&gt; isn't a find action, so it stays &lt;code&gt;type &amp;lt;selector&amp;gt; &amp;lt;text&amp;gt;&lt;/code&gt; at the top level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The token math, and when to pay more
&lt;/h2&gt;

&lt;p&gt;A screenshot of a normal app page runs into the low thousands of tokens, as an image, every time you take one. The accessibility snapshot of that same page is a few hundred tokens of text. I haven't benchmarked this properly, so treat those as the order of magnitude I watched my context fill up at rather than measured figures, but the gap is not subtle and over a ten step flow it shows up on the bill.&lt;/p&gt;

&lt;p&gt;The second order effect interests me more. Because the snapshot is small and structured, Sonnet handles this work fine. Browser driving is repetitive tool calling rather than reasoning, and paying Opus rates to click buttons is silly. You can flip the whole session with &lt;code&gt;/model sonnet&lt;/code&gt;, or keep Opus as the driver and hand browser tasks to a Sonnet subagent, which reaches the same session-connected MCP tools without any extra wiring.&lt;/p&gt;

&lt;p&gt;None of that means throwing away chrome-devtools-mcp. It sees things agent-browser doesn't: full network request bodies, structured console output, Lighthouse audits, performance traces, heap snapshots. When I'm working out why a request 500s or why a page feels slow, that's the tool I want. So the split I've landed on is agent-browser to drive and chrome-devtools-mcp to investigate, on the same browser, so switching mid-task costs nothing. I'll admit I'm not certain this is the right factoring rather than just where I stopped tuning it. It has held up for a few months of daily use.&lt;/p&gt;

&lt;p&gt;Pointing the MCP server at the same Chrome is one line of config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chrome-devtools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chrome-devtools-mcp@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:9222"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's Claude Code's &lt;code&gt;~/.claude.json&lt;/code&gt;. Gemini CLI takes the same shape in &lt;code&gt;~/.gemini/settings.json&lt;/code&gt; and Copilot CLI in &lt;code&gt;~/.copilot/mcp-config.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For Claude Code, add it user-scoped so every session in every directory picks it up, and add it through the CLI rather than hand-editing the file. Claude Code rewrites &lt;code&gt;~/.claude.json&lt;/code&gt; while it's running and will happily clobber a manual edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--scope&lt;/span&gt; user chrome-devtools &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  npx &lt;span class="nt"&gt;-y&lt;/span&gt; chrome-devtools-mcp@latest &lt;span class="nt"&gt;-u&lt;/span&gt; http://127.0.0.1:9222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotchas that cost me real hours
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A portproxy ghost squatting on 9222
&lt;/h3&gt;

&lt;p&gt;This is my favourite of the three, partly because I couldn't find it documented anywhere and partly because it survives every reset you'd instinctively reach for.&lt;/p&gt;

&lt;p&gt;The symptom: Chrome won't start on 9222, nothing you know about is listening, &lt;code&gt;curl&lt;/code&gt; on the CDP endpoint returns nothing at all, and &lt;code&gt;netstat&lt;/code&gt; insists the port is taken. &lt;code&gt;wsl --shutdown&lt;/code&gt; doesn't clear it and neither does a reboot, which is what makes it so disorienting.&lt;/p&gt;

&lt;p&gt;The cause is leftover &lt;code&gt;netsh interface portproxy&lt;/code&gt; rules from the old pre-Mirrored era of WSL port bridging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;0.0.0.0:9222       -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;127.0.0.1:9222
&lt;span class="gp"&gt;172.24.160.1:9222  -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;127.0.0.1:9222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are hosted by the Windows IP Helper service, &lt;code&gt;iphlpsvc&lt;/code&gt;, running inside &lt;code&gt;svchost.exe&lt;/code&gt;, which is why netstat blames svchost. They're persistent Windows configuration rather than a WSL artifact, so rebooting was never going to help. Worth noticing the &lt;code&gt;0.0.0.0&lt;/code&gt; listen address too, since that rule had been quietly exposing 9222 to the whole LAN for who knows how long.&lt;/p&gt;

&lt;p&gt;Find it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/mnt/c/Windows/System32/netstat.exe &lt;span class="nt"&gt;-ano&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;':9222'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; LISTENING
/mnt/c/Windows/System32/tasklist.exe /svc /fi &lt;span class="s2"&gt;"pid eq &amp;lt;PID&amp;gt;"&lt;/span&gt;   &lt;span class="c"&gt;# shows iphlpsvc&lt;/span&gt;
powershell.exe &lt;span class="nt"&gt;-Command&lt;/span&gt; &lt;span class="s2"&gt;"netsh interface portproxy show all"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete it from an elevated PowerShell, since portproxy is protected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;netsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;portproxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v4tov4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;listenport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listenaddress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;netsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;portproxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v4tov4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;listenport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;9222&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;listenaddress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;172.24&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;160&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;netsh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;interface&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;portproxy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The socket frees immediately, with no reboot and no &lt;code&gt;wsl --shutdown&lt;/code&gt; needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Empty tool list, or "Chrome DevTools is not running"
&lt;/h3&gt;

&lt;p&gt;If your setup wraps the MCP server in something that waits for CDP and then degrades to a noop server instead of crashing, a wrong port looks identical to a broken install. I spent a while on this with one session driving the browser perfectly while another insisted nothing was running, and the answer was that their configs pointed at different ports and only one matched the Chrome I'd actually launched.&lt;/p&gt;

&lt;p&gt;When the tools vanish, suspect the port before you suspect anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"browser-url&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;--browser"&lt;/span&gt; ~/.claude.json
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9222/json/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same trap catches you with a per-repo launcher that auto-picks a free port. Repo A takes 9222, repo B takes 9223, and now there are two Chromes and only one of them has your login. Pin the port explicitly if you want a single shared browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP started before Chrome
&lt;/h3&gt;

&lt;p&gt;MCP servers connect at startup. Start one before Chrome exists and it holds a dead session, after which every call fails with &lt;code&gt;Protocol error (Target.setDiscoverTargets): Target closed&lt;/code&gt; while &lt;code&gt;curl&lt;/code&gt; on 9222 looks perfectly healthy. Chrome first, then &lt;code&gt;/mcp&lt;/code&gt; and reconnect. It's a small thing but the error message points nowhere near the actual cause, so it's worth having in your head before you meet it at 1am.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security, briefly
&lt;/h2&gt;

&lt;p&gt;Any local process that can reach &lt;code&gt;localhost:9222&lt;/code&gt; can fully drive that browser, which is simply how CDP works, and two details keep that manageable. Chrome has bound &lt;code&gt;--remote-debugging-port&lt;/code&gt; to 127.0.0.1 only since version 111, so it isn't reachable from other machines unless you deliberately add &lt;code&gt;--remote-debugging-address=0.0.0.0&lt;/code&gt;, which I'd think hard about before doing. And the isolated &lt;code&gt;--user-data-dir&lt;/code&gt; means the blast radius is whatever you signed into inside the debug profile rather than your entire browsing life, which is a good reason to keep that profile boring: your dev app and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health check
&lt;/h2&gt;

&lt;p&gt;When something breaks, paste this before you start theorising:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Is the logged-in Chrome actually up on 9222?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9222/json/version | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool

&lt;span class="c"&gt;# What pages does it have open?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:9222/json/list

&lt;span class="c"&gt;# Which port is each MCP config pointing at? All should say 9222.&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"browser-url&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;--browser"&lt;/span&gt; ~/.claude.json

&lt;span class="c"&gt;# Are the MCP servers connected?&lt;/span&gt;
claude mcp list | &lt;span class="nb"&gt;grep &lt;/span&gt;chrome-devtools

&lt;span class="c"&gt;# Is a stale portproxy rule squatting on 9222? Should be empty.&lt;/span&gt;
powershell.exe &lt;span class="nt"&gt;-Command&lt;/span&gt; &lt;span class="s2"&gt;"netsh interface portproxy show all"&lt;/span&gt;

&lt;span class="c"&gt;# agent-browser socket dir present?&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ld&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_RUNTIME_DIR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;/run/user/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nearly every problem I've had with this setup was one of those lines returning something I didn't expect. The portproxy rule was the exception and it cost me most of an afternoon, so it's in the list now.&lt;/p&gt;

</description>
      <category>wsl2</category>
      <category>claude</category>
      <category>chrome</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
