{"id":492864,"date":"2026-08-30T23:50:16","date_gmt":"2026-08-30T23:50:16","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=492864"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=492864","title":{"rendered":"Why Your AI Agent Gets Blocked and Your Chrome Doesn&#8217;t"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Two months ago I gave an agent a simple job: log into a vendor portal, download last month\u2019s invoice PDF, rename it, drop it in a folder. It worked on my laptop. It failed on the server, silently, in a way that took me a full day to understand \u2014 the page loaded, the DOM was there, the login form was there, and the credentials were rejected with a generic error. No CAPTCHA. No block page. Just \u201csomething went wrong.\u201d<\/p>\n<p>The site had decided my agent was a bot several layers before any of that.<\/p>\n<p>I build a browser for this problem, so I\u2019ve now read a lot of block pages. The thing I keep having to explain is that \u201cbot detection\u201d is not one check that you either pass or fail. It\u2019s a stack, and the layers fire in order, and the layer that kills you is almost never the one you\u2019re looking at.<\/p>\n<h3>The order things actually happen in<\/h3>\n<p>Before a single line of your JavaScript runs, the server already has:<\/p>\n<ol>\n<li>\n<p><strong>Your TLS handshake.<\/strong> Cipher suite order, extensions, elliptic curves, ALPN, GREASE placement. This gets hashed into a JA3\/JA4 fingerprint. A Go or Python HTTP client claiming to be Chrome 140 is over the moment the handshake completes \u2014 the UA says Chrome, the handshake says <code>crypto\/tls<\/code>. That\u2019s not a heuristic, it\u2019s a contradiction.<\/p>\n<\/li>\n<li>\n<p><strong>Your HTTP\/2 frames.<\/strong> SETTINGS values, initial window size, header table size, the pseudo-header order. Chrome sends a specific shape. Most automation stacks that proxy or rewrite requests don\u2019t.<\/p>\n<\/li>\n<li>\n<p><strong>Your headers.<\/strong> Order matters. Presence matters. <code>Sec-Fetch-Site<\/code>, <code>Sec-Fetch-Mode<\/code>, <code>Sec-CH-UA<\/code> and friends are sent by real Chrome in a specific pattern that depends on how the navigation started. The University of Bamberg\u2019s <em>Detecting Bot Detection<\/em> dataset (arXiv:2606.14525, ARES 2026, Tranco Top 1M) found that <strong>75% of the blocks they observed against headless Chromium could be triggered on HTTP header signals alone<\/strong> \u2014 no JS execution required.<\/p>\n<\/li>\n<\/ol>\n<p>Only after all of that does the page get to run the fingerprinting script everybody writes blog posts about.<\/p>\n<p>This ordering is why \u201cI patched <code>navigator.webdriver<\/code> and it still doesn\u2019t work\u201d is the single most common thing I hear. Of course it doesn\u2019t. You fixed layer 4 of a stack that rejected you at layer 1.<\/p>\n<h3>Why your own Chrome sails through<\/h3>\n<p>Your desktop Chrome passes not because it\u2019s trusted, but because everything about it agrees with everything else about it. That\u2019s the whole game.<\/p>\n<ul>\n<li>\n<p>The TLS fingerprint matches the UA string.<\/p>\n<\/li>\n<li>\n<p>The UA string matches the high-entropy client hints (<code>platformVersion<\/code>, <code>architecture<\/code>, <code>model<\/code>).<\/p>\n<\/li>\n<li>\n<p>The client hints match <code>navigator.platform<\/code>.<\/p>\n<\/li>\n<li>\n<p><code>navigator.platform<\/code> matches the GPU strings that WebGL reports.<\/p>\n<\/li>\n<li>\n<p>The GPU strings match the screen resolution, the device pixel ratio, and the available fonts.<\/p>\n<\/li>\n<li>\n<p>The timezone matches the exit IP\u2019s geography.<\/p>\n<\/li>\n<li>\n<p>The language list matches the locale that <code>Intl.DateTimeFormat().resolvedOptions()<\/code> reports.<\/p>\n<\/li>\n<li>\n<p>And all of that stays the same when the page asks a Web Worker instead of the main thread, or asks from inside an iframe.<\/p>\n<\/li>\n<\/ul>\n<p>An agent stack breaks this by accident, constantly. You set a UA string but not the client hints. You route through a proxy in Frankfurt while your container\u2019s timezone is UTC and your <code>navigator.languages<\/code> is <code>en-US<\/code>. You run headless in a container with software rendering, so WebGL reports SwiftShader or llvmpipe while the UA claims a Windows desktop with an RTX card. Each of those is individually harmless-looking and collectively a signed confession.<\/p>\n<p>The detector doesn\u2019t need to know what a \u201ccorrect\u201d machine looks like. It only needs to find two of your claims that can\u2019t both be true.<\/p>\n<h3>The three failure modes I see most<\/h3>\n<p><strong>1. Half-overrides.<\/strong> Someone overrides <code>navigator.platform<\/code> in an injected script but leaves the CDP-level UA metadata untouched, or vice versa. Now the main thread says one thing and the browser\u2019s own protocol-level state says another. Same for the connection info: <code>navigator.connection<\/code> exposes <code>rtt<\/code>, <code>downlink<\/code> and <code>effectiveType<\/code>, and real Chrome derives all three from one measurement. If you spoof <code>effectiveType: '4g'<\/code> and leave <code>rtt: 0<\/code>, you\u2019ve published a value combination Chrome never emits. (When we derive that object, we measure the actual round-trip through the proxy and run it through Chromium\u2019s own effective-connection-type thresholds, so the three fields stay consistent by construction. Deriving them separately is how you get a contradiction.)<\/p>\n<p><strong>2. Environment leaking through the disguise.<\/strong> This is the one that bit me hardest. Suppress the font enumeration surface all you like \u2014 if the host OS is a Chinese Windows install, some CSS system font keywords (<code>menu<\/code>, <code>small-caption<\/code>, <code>status-bar<\/code>) can still resolve through a code path that isn\u2019t the one you patched, and the measured metrics come back as a CJK UI font on a persona that claims to be a US English machine. The fingerprint surface was clean. The <em>rendering<\/em> wasn\u2019t. Detectors measure rendering.<\/p>\n<p><strong>3. Automation artifacts nobody thinks of as fingerprint surface.<\/strong> A proxy-auth browser extension is the classic. It works, it\u2019s easy, and it puts an enumerable entry in the extension list of a browser that is supposed to look like a stock consumer install. Same category: an extra tab your launcher opened, a window size no human has, a <code>--disable-*<\/code> flag that changes an observable default.<\/p>\n<h3>What to actually do about it<\/h3>\n<p>In rough order of return on effort:<\/p>\n<ul>\n<li>\n<p><strong>Fix the network layer first.<\/strong> If your TLS and HTTP\/2 fingerprints don\u2019t match the browser you claim to be, nothing above matters. This is why real-browser stacks beat HTTP clients on hard targets, and why \u201cjust add headers to <code>requests<\/code>\u201d plateaus fast.<\/p>\n<\/li>\n<li>\n<p><strong>Give the agent a residential-quality exit and make the browser agree with it.<\/strong> Timezone, locale, and language list should be derived from the exit IP, not from your server\u2019s environment. This is cheap and it kills an entire class of contradiction.<\/p>\n<\/li>\n<li>\n<p><strong>Stop injecting overrides from userland where you can avoid it.<\/strong> Anything you do with a <code>page.evaluate<\/code> or an init script runs <em>after<\/em> the page\u2019s own code can install traps, and a modified getter is detectable \u2014 <code>Function.prototype.toString<\/code>, property descriptors, prototype identity across realms. If you can push the change into the browser itself, do that instead. If you can\u2019t, at least make the JS layer and the protocol layer say the same thing.<\/p>\n<\/li>\n<li>\n<p><strong>Keep the profile.<\/strong> A fresh profile on every run means no cookies, no history, no site engagement \u2014 the exact shape of a throwaway. Persisting a real profile directory across runs does more for pass rates than most fingerprint tuning.<\/p>\n<\/li>\n<li>\n<p><strong>Behave like a session, not a burst.<\/strong> Some vendors build a behavioral picture from the first request. Mouse-free, perfectly-timed, straight-to-the-endpoint navigation is a signal in itself, independent of every fingerprint value.<\/p>\n<\/li>\n<\/ul>\n<h3>The uncomfortable part<\/h3>\n<p>None of this makes you undetectable, and I\u2019d distrust anyone who tells you otherwise. The Bamberg data also found that enterprise-grade bot management (the stuff that does behavioral ML per customer) sits on well under 1% of the web overall \u2014 but around 15% of the sites people actually want to automate. The hard targets are hard on purpose, and against the top tier the deciding factors turn out to be exit IP quality, account age, and behavior, not which browser build you used.<\/p>\n<p>What the browser layer gets you is the removal of <em>cheap<\/em> reasons to block you. That\u2019s not nothing. Most of the failures I debug are cheap reasons \u2014 a header, a timezone, a WebGL string. Fix those, and you find out whether you had a real problem in the first place.<\/p>\n<p>If your agent is failing today, don\u2019t start with the fingerprint. Start with a request log and find out how far you got before it went wrong. Nine times out of ten the answer is: not as far as you assumed.<\/p>\n<hr\/>\n<p>\u0412\u043f\u0435\u0440\u0432\u044b\u0435 \u043e\u043f\u0443\u0431\u043b\u0438\u043a\u043e\u0432\u0430\u043d\u043e \u0437\u0434\u0435\u0441\u044c: <a href=\"https:\/\/antibrow.com\/blog\/why-your-ai-agent-gets-blocked\" rel=\"noopener nofollow\">https:\/\/antibrow.com\/blog\/why-your-ai-agent-gets-blocked<\/a><\/p>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1076492\/\">https:\/\/habr.com\/ru\/articles\/1076492\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Two months ago I gave an agent a simple job: log into a vendor portal, download last month\u2019s invoice PDF, rename it, drop it in a folder. It worked on my laptop. It failed on the server, silently, in a way that took me a full day to understand \u2014 the page loaded, the DOM was there, the login form was there, and the credentials were rejected with a generic error. No CAPTCHA. No block page. Just \u201csomething went wrong.\u201dThe site had decided my agent was a bot several layers before any of that.I build a browser for this problem, so I\u2019ve now read a lot of block pages. The thing I keep having to explain is that \u201cbot detection\u201d is not one check that you either pass or fail. It\u2019s a stack, and the layers fire in order, and the layer that kills you is almost never the one you\u2019re looking at.The order things actually happen inBefore a single line of your JavaScript runs, the server already has:Your TLS handshake. Cipher suite order, extensions, elliptic curves, ALPN, GREASE placement. This gets hashed into a JA3\/JA4 fingerprint. A Go or Python HTTP client claiming to be Chrome 140 is over the moment the handshake completes \u2014 the UA says Chrome, the handshake says crypto\/tls. That\u2019s not a heuristic, it\u2019s a contradiction.Your HTTP\/2 frames. SETTINGS values, initial window size, header table size, the pseudo-header order. Chrome sends a specific shape. Most automation stacks that proxy or rewrite requests don\u2019t.Your headers. Order matters. Presence matters. Sec-Fetch-Site, Sec-Fetch-Mode, Sec-CH-UA and friends are sent by real Chrome in a specific pattern that depends on how the navigation started. The University of Bamberg\u2019s Detecting Bot Detection dataset (arXiv:2606.14525, ARES 2026, Tranco Top 1M) found that 75% of the blocks they observed against headless Chromium could be triggered on HTTP header signals alone \u2014 no JS execution required.Only after all of that does the page get to run the fingerprinting script everybody writes blog posts about.This ordering is why \u201cI patched navigator.webdriver and it still doesn\u2019t work\u201d is the single most common thing I hear. Of course it doesn\u2019t. You fixed layer 4 of a stack that rejected you at layer 1.Why your own Chrome sails throughYour desktop Chrome passes not because it\u2019s trusted, but because everything about it agrees with everything else about it. That\u2019s the whole game.The TLS fingerprint matches the UA string.The UA string matches the high-entropy client hints (platformVersion, architecture, model).The client hints match navigator.platform.navigator.platform matches the GPU strings that WebGL reports.The GPU strings match the screen resolution, the device pixel ratio, and the available fonts.The timezone matches the exit IP\u2019s geography.The language list matches the locale that Intl.DateTimeFormat().resolvedOptions() reports.And all of that stays the same when the page asks a Web Worker instead of the main thread, or asks from inside an iframe.An agent stack breaks this by accident, constantly. You set a UA string but not the client hints. You route through a proxy in Frankfurt while your container\u2019s timezone is UTC and your navigator.languages is en-US. You run headless in a container with software rendering, so WebGL reports SwiftShader or llvmpipe while the UA claims a Windows desktop with an RTX card. Each of those is individually harmless-looking and collectively a signed confession.The detector doesn\u2019t need to know what a \u201ccorrect\u201d machine looks like. It only needs to find two of your claims that can\u2019t both be true.The three failure modes I see most1. Half-overrides. Someone overrides navigator.platform in an injected script but leaves the CDP-level UA metadata untouched, or vice versa. Now the main thread says one thing and the browser\u2019s own protocol-level state says another. Same for the connection info: navigator.connection exposes rtt, downlink and effectiveType, and real Chrome derives all three from one measurement. If you spoof effectiveType: &#8216;4g&#8217; and leave rtt: 0, you\u2019ve published a value combination Chrome never emits. (When we derive that object, we measure the actual round-trip through the proxy and run it through Chromium\u2019s own effective-connection-type thresholds, so the three fields stay consistent by construction. Deriving them separately is how you get a contradiction.)2. Environment leaking through the disguise. This is the one that bit me hardest. Suppress the font enumeration surface all you like \u2014 if the host OS is a Chinese Windows install, some CSS system font keywords (menu, small-caption, status-bar) can still resolve through a code path that isn\u2019t the one you patched, and the measured metrics come back as a CJK UI font on a persona that claims to be a US English machine. The fingerprint surface was clean. The rendering wasn\u2019t. Detectors measure rendering.3. Automation artifacts nobody thinks of as fingerprint surface. A proxy-auth browser extension is the classic. It works, it\u2019s easy, and it puts an enumerable entry in the extension list of a browser that is supposed to look like a stock consumer install. Same category: an extra tab your launcher opened, a window size no human has, a &#8212;disable-* flag that changes an observable default.What to actually do about itIn rough order of return on effort:Fix the network layer first. If your TLS and HTTP\/2 fingerprints don\u2019t match the browser you claim to be, nothing above matters. This is why real-browser stacks beat HTTP clients on hard targets, and why \u201cjust add headers to requests\u201d plateaus fast.Give the agent a residential-quality exit and make the browser agree with it. Timezone, locale, and language list should be derived from the exit IP, not from your server\u2019s environment. This is cheap and it kills an entire class of contradiction.Stop injecting overrides from userland where you can avoid it. Anything you do with a page.evaluate or an init script runs after the page\u2019s own code can install traps, and a modified getter is detectable \u2014 Function.prototype.toString, property descriptors, prototype identity across realms. If you can push the change into the browser itself, do that instead. If you can\u2019t, at least make the JS layer and the protocol layer say the same thing.Keep the profile. A fresh profile on every run means no cookies, no history, no site engagement \u2014 the exact shape of a throwaway. Persisting a real profile directory across runs does more for pass rates than most fingerprint tuning.Behave like a session, not a burst. Some vendors build a behavioral picture from the first request. Mouse-free, perfectly-timed, straight-to-the-endpoint navigation is a signal in itself, independent of every fingerprint value.The uncomfortable partNone of this makes you undetectable, and I\u2019d distrust anyone who tells you otherwise. The Bamberg data also found that enterprise-grade bot management (the stuff that does behavioral ML per customer) sits on well under 1% of the web overall \u2014 but around 15% of the sites people actually want to automate. The hard targets are hard on purpose, and against the top tier the deciding factors turn out to be exit IP quality, account age, and behavior, not which browser build you used.What the browser layer gets you is the removal of cheap reasons to block you. That\u2019s not nothing. Most of the failures I debug are cheap reasons \u2014 a header, a timezone, a WebGL string. Fix those, and you find out whether you had a real problem in the first place.If your agent is failing today, don\u2019t start with the fingerprint. Start with a request log and find out how far you got before it went wrong. Nine times out of ten the answer is: not as far as you assumed.\u0412\u043f\u0435\u0440\u0432\u044b\u0435 \u043e\u043f\u0443\u0431\u043b\u0438\u043a\u043e\u0432\u0430\u043d\u043e \u0437\u0434\u0435\u0441\u044c: https:\/\/antibrow.com\/blog\/why-your-ai-agent-gets-blocked\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 https:\/\/habr.com\/ru\/articles\/1076492\/<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-492864","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/492864","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=492864"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/492864\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=492864"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=492864"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=492864"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}