Comparison
Chrome DevTools MCP Gets a CAPTCHA. Claude in Chrome Doesn't.
· 6 min read
On this page
I keep a list of article ideas and I don’t write any of them until I’ve looked at the search results for the query. That check runs through a browser: open Google, read page one, decide whether the seat is taken. It’s the least interesting automation I own and it had never once failed.
Today it failed. The Claude in Chrome extension wasn’t connected, so the agent fell back to the other browser path it had — Chrome DevTools MCP, which drives Chrome over the DevTools Protocol. The first search redirected to /sorry/index, Google’s CAPTCHA wall. I reconnected the extension, ran the identical query, and got a normal results page.
Two browser paths on one Mac, one blocked and one not. Every article ranking for this problem explains it the same way: sites detect the CDP connection, so you need a stealth plugin. I measured both paths instead, and the CDP story doesn’t hold up.
What each path actually is
Claude in Chrome is a browser extension. There is no separate browser — it acts inside the Chrome I already have open, with my profile, my cookies, and my logged-in sessions.
Chrome DevTools MCP is an MCP server that speaks the Chrome DevTools Protocol to a browser it manages. It launches its own Chrome with its own profile.
Playwright MCP — the third option people compare these against — launches a fresh browser context per run, which is the point of it. I didn’t have it installed and didn’t install one just to measure it, so it stays out of the numbers below.
The first two were both live on this machine at the same moment, which is what makes the comparison worth anything: same hardware, same Chrome build, same network, same minute.
Measuring both
I opened https://www.google.com/ on each path and ran the same function through each one’s script-evaluation tool.
() => {
const c = document.createElement('canvas');
const gl = c.getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
ua: navigator.userAgent,
webdriver: navigator.webdriver,
cookieLen: document.cookie.length,
hasSID: /(^|; )SID=/.test(document.cookie),
screen: screen.width + 'x' + screen.height,
webglRenderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
plugins: navigator.plugins.length,
deviceMemory: navigator.deviceMemory,
hwConcurrency: navigator.hardwareConcurrency,
pdfViewer: navigator.pdfViewerEnabled,
chromeKeys: window.chrome ? Object.keys(window.chrome).join(',') : null,
};
}
Results, 2026-08-19:
| Surface | Claude in Chrome | Chrome DevTools MCP |
|---|---|---|
navigator.userAgent | Chrome/151.0.0.0 | HeadlessChrome/151.0.0.0 |
userAgentData.brands | Google Chrome 151, Chromium 151 | Google Chrome 151, Chromium 151 |
navigator.webdriver | false | false |
document.cookie length | 682 | 112 |
Google SID cookie | present | absent |
screen | 1920x1080 | 800x600 |
| WebGL renderer | ANGLE Metal, Apple M4 | ANGLE Metal, Apple M4 |
navigator.plugins.length | 5 | 5 |
deviceMemory / hardwareConcurrency | 16 / 10 | 16 / 10 |
pdfViewerEnabled | true | true |
window.chrome keys | loadTimes,csi,app | loadTimes,csi,app |
| Google search | results page | /sorry/index |
The last row is the outcome, not a surface. Of the eleven fingerprint surfaces above it, seven are byte-identical. That’s the finding.
The two signals everyone writes about are both dead
navigator.webdriver was false on the blocked path. This is the flag that gets named first in every guide, the one puppeteer-extra-plugin-stealth exists to patch. Chrome DevTools MCP already ships with it off. Whatever blocked me, it wasn’t reading that property.
The GPU was real. The other standing rule of thumb is that headless Chrome falls back to SwiftShader, so UNMASKED_RENDERER_WEBGL gives you a software renderer where a real user has a real card. Both paths reported ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version) — the actual GPU in this laptop. Modern headless Chrome uses the hardware.
window.chrome, navigator.plugins, deviceMemory, hardwareConcurrency, pdfViewerEnabled — all the properties that used to give headless away — matched too. The Chrome team closed those gaps. So did the fingerprint-your-own-agent research I wrote about earlier this month, which found the same thing from the other direction: browser features alone barely separate agents from humans (F1 0.80), and it’s behavior that gives them away.
What actually differed
Three things, and none needs a clever detector.
The User-Agent string says HeadlessChrome. Not a subtle inference — the browser announces it in a header on every single request, before any JavaScript runs. Note the row above it: userAgentData.brands reports plain “Google Chrome 151” on both paths. Client Hints don’t carry the headless marker. The legacy UA string does, and that’s the one you have to opt out of sending.
screen is 800x600. The default virtual display of a headless Chrome. Nobody browses at 800x600 on an M4 laptop with a 1920x1080 screen, and the pair “Mac, 16 GB, M4 GPU, 800x600” is incoherent in a way that needs no machine learning to spot.
There was no session. 112 bytes of cookies against 682, no SID, and a sign-in link in the page — a brand-new profile that had never seen google.com. The extension path is my everyday browser: logged in, with history behind it.
Any one of those is enough on its own. The CDP connection never had to be detected, because a request arrived saying it came from a headless browser at 800x600 with no account, and got treated accordingly.
One measurement I’d throw out: window.outerWidth read 1440 on the headless path and 0 on the extension path — the opposite of what you’d expect. That’s the extension’s isolated execution context, not the browser. It’s a good reminder that you’re measuring the tool as much as the browser, which is exactly why the identical rows above are the ones that carry weight.
This is not a bypass guide
The search results for this problem are mostly stealth plugins and unblocking APIs. I’m not going to add to them, and I didn’t try to defeat the CAPTCHA — my agent is under instructions not to, and the instruction is right. Google serving an interstitial to a headless browser with no session is Google working correctly. If you want search results in a program, the supported route is the API, or a human-driven session where a human is actually present.
The useful question isn’t how to look human. It’s which path to pick.
Picking a path
Work that needs your identity → the extension. Reading a logged-in dashboard, checking what a search engine shows you, anything behind SSO. A managed browser starts with an empty cookie jar, and the fix for that is either logging a robot into your accounts or copying a profile, both of which are worse than they sound.
Work on public pages → Chrome DevTools MCP. Performance traces, console errors, network waterfalls, layout debugging. It gives targeted protocol-level data the extension can’t, the empty profile is a feature for reproducibility, and none of it involves a site that cares who you are.
Work that must run unattended → Playwright MCP. No local Chrome to keep alive, no extension to stay connected — which is exactly the failure I hit today. It’s also the path most likely to meet a bot wall, so point it at systems you control.
The dividing line is whether the target cares who’s asking. My SERP check does, and I’d quietly built it on a path that couldn’t answer — it only ever worked because the extension happened to be connected every previous time.
Worth noting how cheap the detection was on Google’s side. No behavioral analysis, no mouse-movement model, no CDP probe. A header and a screen size. It’s the same asymmetry as AI crawlers and JavaScript: the interesting technical question (can you fingerprint an agent?) is downstream of a much duller one (what is it announcing about itself?), and the duller one decides the outcome.
If you’re wiring browser tools into an agent, run the function above on each path before you trust it. Ten minutes of measuring told me more than the page of search results did.