<?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: Santiago Blaine</title>
    <description>The latest articles on DEV Community by Santiago Blaine (@santiago_blaine_f319c228c).</description>
    <link>https://dev.to/santiago_blaine_f319c228c</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%2F4091475%2F198a2128-3cc9-426a-9b01-f7711e18d112.png</url>
      <title>DEV Community: Santiago Blaine</title>
      <link>https://dev.to/santiago_blaine_f319c228c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/santiago_blaine_f319c228c"/>
    <language>en</language>
    <item>
      <title>Your Desktop Anti-Detect Setup Doesn't Survive Mobile</title>
      <dc:creator>Santiago Blaine</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:03:10 +0000</pubDate>
      <link>https://dev.to/santiago_blaine_f319c228c/your-desktop-anti-detect-setup-doesnt-survive-mobile-4h9h</link>
      <guid>https://dev.to/santiago_blaine_f319c228c/your-desktop-anti-detect-setup-doesnt-survive-mobile-4h9h</guid>
      <description>&lt;p&gt;When I added mobile personas to a browser that already had desktop ones working, I assumed the work was mostly string substitution: swap the user agent, set touch points to 5, shrink the viewport, done. That assumption survived about a day.&lt;/p&gt;

&lt;p&gt;Mobile fingerprints aren't a variant of desktop fingerprints. They're a different set of constraints, and most of the desktop toolkit either doesn't apply or actively gives you away. Here's what I ran into, in the order it hurt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The user agent has almost no room left
&lt;/h2&gt;

&lt;p&gt;Desktop anti-detect work still has a habit of treating the UA string as a place to be creative. On mobile, UA reduction has frozen the format into a small number of shapes. There is essentially one correct form for Chrome on an Android phone, one for an Android tablet, and one for desktop Windows. Everything else — every extra token, every version in the wrong segment — is a mismatch against a string the detector can generate itself.&lt;/p&gt;

&lt;p&gt;This turned out to be the single most effective filter when I was building a library of real device profiles. I collect fingerprints from consenting real visitors, and the first rule is: &lt;strong&gt;the UA must match one of the known-good forms character for character.&lt;/strong&gt; A whitelist, not a blacklist. It rejects Edge, Samsung Internet, Opera and Yandex builds in one move — which matters because the engine reports Chrome branding, so a Samsung Internet UA sitting in the same profile row is a contradiction, not a variation.&lt;/p&gt;

&lt;p&gt;It also catches something the collection side gets wrong: in-app webviews. Facebook's, Instagram's and TikTok's embedded browsers announce themselves in the UA (&lt;code&gt;; wv)&lt;/code&gt; plus an app token), and my collector's own webview flag missed a batch of them. The UA string didn't. Never trust the capture layer's classification when the raw string is right there.&lt;/p&gt;

&lt;p&gt;Webviews matter because their font sets and available APIs are the host app's, not a stock browser's. A profile captured from one is a profile of a thing your browser can't be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Client hints are where mobile identity actually lives
&lt;/h2&gt;

&lt;p&gt;On desktop you can often get away with ignoring high-entropy client hints. On Android you cannot, because &lt;code&gt;model&lt;/code&gt; and &lt;code&gt;platformVersion&lt;/code&gt; are the two fields that say &lt;em&gt;which phone this is&lt;/em&gt;, and they have to agree with everything else.&lt;/p&gt;

&lt;p&gt;The failure mode is subtle. If your persona lacks these values, something has to fill them in — and a generated placeholder like a round &lt;code&gt;15.0.0&lt;/code&gt; platform version with an empty model is a fingerprint of your generator, not of a phone. So my import rules drop any captured device missing them. Those records are still a real device; they're just no longer usable as one.&lt;/p&gt;

&lt;p&gt;Same reasoning applies to what you might call the "identity cross-check": &lt;code&gt;navigator.platform&lt;/code&gt;, the client-hint platform, and the UA all have to tell the same story. Any disagreement is an automatic reject on the way in, because a contradiction shipped to a user is worse than a smaller library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GPU is the tell
&lt;/h2&gt;

&lt;p&gt;Android GPUs are Adreno, Mali, PowerVR, Xclipse. They report through OpenGL ES with a characteristic renderer string, a specific set of supported extensions, and specific shader precision ranges.&lt;/p&gt;

&lt;p&gt;Two rejects I check for explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Software renderers.&lt;/strong&gt; SwiftShader or llvmpipe in a mobile profile means it came from an emulator or a headless run, not a phone. Those get dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct3D on Android.&lt;/strong&gt; A profile claiming to be an Android phone whose WebGL layer reports a D3D11 backend is a Windows machine wearing a mobile UA. This is exactly what a desktop-first spoofing setup produces when it only rewrites strings, and it's trivially detectable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second one is the general lesson: &lt;strong&gt;mobile spoofing fails on the surfaces that are computed rather than declared.&lt;/strong&gt; You can declare a UA. You cannot declare the shader precision ranges the driver reports, unless the substitution happens deep enough in the browser to replace the real answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker threads and iframes have to agree
&lt;/h2&gt;

&lt;p&gt;The check that catches the most half-finished spoofing setups is also the cheapest to run: ask the same question from a Web Worker, and from an iframe, that you asked on the main thread.&lt;/p&gt;

&lt;p&gt;A userland override installed on the main thread's &lt;code&gt;navigator&lt;/code&gt; does not exist inside a freshly created worker. So a page reads "Android, model X" on the main thread and "Linux x86_64, no model" from the worker, and it doesn't need any other evidence. I reject captured devices where the two disagree, for the same reason — that device was itself running something that only patched the main thread, and I'd be importing someone else's broken disguise as ground truth.&lt;/p&gt;

&lt;p&gt;Anything that only lives above the engine has this problem structurally. Every new realm — worker, iframe, &lt;code&gt;about:blank&lt;/code&gt; document — is a fresh set of natives your injection has to race and re-apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screen geometry is a joint constraint
&lt;/h2&gt;

&lt;p&gt;Desktop personas can get away with a viewport that's approximately plausible. Mobile ones can't, because the values are coupled:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen dimensions, device pixel ratio, available screen area and viewport size are all related through one device's actual hardware.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;navigator.maxTouchPoints&lt;/code&gt; must be non-zero, and a "mobile" persona with a mouse-shaped pointer media query is a contradiction.&lt;/li&gt;
&lt;li&gt;Tablets and phones have different UA forms &lt;em&gt;and&lt;/em&gt; different aspect ratios, and mixing the two is a common generated-persona failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The efficient answer here is not to generate these values but to sample them together from a real device, as one unit. Which is why I run a real-device library at all rather than a generator: the joint distribution is the hard part, and real hardware gives it to you for free.&lt;/p&gt;

&lt;p&gt;That library has a number I like quoting because it's the honest cost of the rules above: after all the filters, &lt;strong&gt;roughly one in nine collected devices is usable.&lt;/strong&gt; Everything else fails on a webview UA, a mismatched platform, a missing client hint, a software renderer, or a worker-thread disagreement. Collecting a fake is much worse than collecting nothing, because a fake gets served to users under the label "real device."&lt;/p&gt;

&lt;h2&gt;
  
  
  What mobile still can't do
&lt;/h2&gt;

&lt;p&gt;Being straight about the ceiling: the platforms people most want mobile personas for — the big social apps — largely don't care about your browser fingerprint, because their real defenses are in their native apps. Request signing, device attestation and behavioral models built on native telemetry are not things a web browser can present, no matter how good the persona is. Community reports about account survival on those platforms are consistent and unflattering to every product in this category, mine included.&lt;/p&gt;

&lt;p&gt;Where mobile personas do earn their keep is everything else: mobile-only or mobile-preferential web experiences, testing responsive behavior against real device profiles rather than devtools emulation, and any target whose mobile web path is less hostile than its desktop one.&lt;/p&gt;

&lt;p&gt;Just don't reach for it thinking it's a desktop profile with a different UA. The UA is the one part you almost can't get wrong, because there's only one right answer. It's everything computed downstream that decides.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>mobile</category>
      <category>security</category>
    </item>
    <item>
      <title>Why Your AI Agent Gets Blocked and Your Chrome Doesn't</title>
      <dc:creator>Santiago Blaine</dc:creator>
      <pubDate>Sun, 30 Aug 2026 23:43:59 +0000</pubDate>
      <link>https://dev.to/santiago_blaine_f319c228c/why-your-ai-agent-gets-blocked-and-your-chrome-doesnt-57ch</link>
      <guid>https://dev.to/santiago_blaine_f319c228c/why-your-ai-agent-gets-blocked-and-your-chrome-doesnt-57ch</guid>
      <description>&lt;p&gt;Two months ago I gave an agent a simple job: log into a vendor portal, download last month's 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 — 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 "something went wrong."&lt;/p&gt;

&lt;p&gt;The site had decided my agent was a bot several layers before any of that.&lt;/p&gt;

&lt;p&gt;I build a browser for this problem, so I've now read a lot of block pages. The thing I keep having to explain is that "bot detection" is not one check that you either pass or fail. It's a stack, and the layers fire in order, and the layer that kills you is almost never the one you're looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The order things actually happen in
&lt;/h2&gt;

&lt;p&gt;Before a single line of your JavaScript runs, the server already has:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your TLS handshake.&lt;/strong&gt; 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 — the UA says Chrome, the handshake says &lt;code&gt;crypto/tls&lt;/code&gt;. That's not a heuristic, it's a contradiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your HTTP/2 frames.&lt;/strong&gt; 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't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your headers.&lt;/strong&gt; Order matters. Presence matters. &lt;code&gt;Sec-Fetch-Site&lt;/code&gt;, &lt;code&gt;Sec-Fetch-Mode&lt;/code&gt;, &lt;code&gt;Sec-CH-UA&lt;/code&gt; and friends are sent by real Chrome in a specific pattern that depends on how the navigation started. The University of Bamberg's &lt;em&gt;Detecting Bot Detection&lt;/em&gt; dataset (arXiv:2606.14525, ARES 2026, Tranco Top 1M) found that &lt;strong&gt;75% of the blocks they observed against headless Chromium could be triggered on HTTP header signals alone&lt;/strong&gt; — no JS execution required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Only after all of that does the page get to run the fingerprinting script everybody writes blog posts about.&lt;/p&gt;

&lt;p&gt;This ordering is why "I patched &lt;code&gt;navigator.webdriver&lt;/code&gt; and it still doesn't work" is the single most common thing I hear. Of course it doesn't. You fixed layer 4 of a stack that rejected you at layer 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your own Chrome sails through
&lt;/h2&gt;

&lt;p&gt;Your desktop Chrome passes not because it's trusted, but because everything about it agrees with everything else about it. That's the whole game.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TLS fingerprint matches the UA string.&lt;/li&gt;
&lt;li&gt;The UA string matches the high-entropy client hints (&lt;code&gt;platformVersion&lt;/code&gt;, &lt;code&gt;architecture&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The client hints match &lt;code&gt;navigator.platform&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;navigator.platform&lt;/code&gt; matches the GPU strings that WebGL reports.&lt;/li&gt;
&lt;li&gt;The GPU strings match the screen resolution, the device pixel ratio, and the available fonts.&lt;/li&gt;
&lt;li&gt;The timezone matches the exit IP's geography.&lt;/li&gt;
&lt;li&gt;The language list matches the locale that &lt;code&gt;Intl.DateTimeFormat().resolvedOptions()&lt;/code&gt; reports.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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's timezone is UTC and your &lt;code&gt;navigator.languages&lt;/code&gt; is &lt;code&gt;en-US&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;The detector doesn't need to know what a "correct" machine looks like. It only needs to find two of your claims that can't both be true.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three failure modes I see most
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Half-overrides.&lt;/strong&gt; Someone overrides &lt;code&gt;navigator.platform&lt;/code&gt; 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's own protocol-level state says another. Same for the connection info: &lt;code&gt;navigator.connection&lt;/code&gt; exposes &lt;code&gt;rtt&lt;/code&gt;, &lt;code&gt;downlink&lt;/code&gt; and &lt;code&gt;effectiveType&lt;/code&gt;, and real Chrome derives all three from one measurement. If you spoof &lt;code&gt;effectiveType: '4g'&lt;/code&gt; and leave &lt;code&gt;rtt: 0&lt;/code&gt;, you've 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's own effective-connection-type thresholds, so the three fields stay consistent by construction. Deriving them separately is how you get a contradiction.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Environment leaking through the disguise.&lt;/strong&gt; This is the one that bit me hardest. Suppress the font enumeration surface all you like — if the host OS is a Chinese Windows install, some CSS system font keywords (&lt;code&gt;menu&lt;/code&gt;, &lt;code&gt;small-caption&lt;/code&gt;, &lt;code&gt;status-bar&lt;/code&gt;) can still resolve through a code path that isn't 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 &lt;em&gt;rendering&lt;/em&gt; wasn't. Detectors measure rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Automation artifacts nobody thinks of as fingerprint surface.&lt;/strong&gt; A proxy-auth browser extension is the classic. It works, it's 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 &lt;code&gt;--disable-*&lt;/code&gt; flag that changes an observable default.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do about it
&lt;/h2&gt;

&lt;p&gt;In rough order of return on effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fix the network layer first.&lt;/strong&gt; If your TLS and HTTP/2 fingerprints don't match the browser you claim to be, nothing above matters. This is why real-browser stacks beat HTTP clients on hard targets, and why "just add headers to &lt;code&gt;requests&lt;/code&gt;" plateaus fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the agent a residential-quality exit and make the browser agree with it.&lt;/strong&gt; Timezone, locale, and language list should be derived from the exit IP, not from your server's environment. This is cheap and it kills an entire class of contradiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop injecting overrides from userland where you can avoid it.&lt;/strong&gt; Anything you do with a &lt;code&gt;page.evaluate&lt;/code&gt; or an init script runs &lt;em&gt;after&lt;/em&gt; the page's own code can install traps, and a modified getter is detectable — &lt;code&gt;Function.prototype.toString&lt;/code&gt;, property descriptors, prototype identity across realms. If you can push the change into the browser itself, do that instead. If you can't, at least make the JS layer and the protocol layer say the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the profile.&lt;/strong&gt; A fresh profile on every run means no cookies, no history, no site engagement — the exact shape of a throwaway. Persisting a real profile directory across runs does more for pass rates than most fingerprint tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behave like a session, not a burst.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;None of this makes you undetectable, and I'd 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 — 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.&lt;/p&gt;

&lt;p&gt;What the browser layer gets you is the removal of &lt;em&gt;cheap&lt;/em&gt; reasons to block you. That's not nothing. Most of the failures I debug are cheap reasons — a header, a timezone, a WebGL string. Fix those, and you find out whether you had a real problem in the first place.&lt;/p&gt;

&lt;p&gt;If your agent is failing today, don't 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.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>I Built a Browser That Passes CreepJS — Here's What Still Catches Me</title>
      <dc:creator>Santiago Blaine</dc:creator>
      <pubDate>Mon, 24 Aug 2026 20:01:44 +0000</pubDate>
      <link>https://dev.to/santiago_blaine_f319c228c/i-built-a-browser-that-passes-creepjs-heres-what-still-catches-me-1gfn</link>
      <guid>https://dev.to/santiago_blaine_f319c228c/i-built-a-browser-that-passes-creepjs-heres-what-still-catches-me-1gfn</guid>
      <description>&lt;p&gt;CreepJS is the test everyone in this space quotes and almost nobody reads carefully. It's an open-source page that fingerprints your browser and then — this is the part that matters — checks whether your browser is &lt;em&gt;lying&lt;/em&gt;. Not whether your values are unusual. Whether they're internally possible.&lt;/p&gt;

&lt;p&gt;I spent a long stretch getting a browser through it. It passes now, mostly. I want to write down what actually moved the needle, and then be specific about the part that still catches me, because a post that ends with "and then it was perfect" is a post you shouldn't believe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CreepJS is really testing
&lt;/h2&gt;

&lt;p&gt;Most fingerprint pages collect values and show you a hash. CreepJS collects values and then attacks them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native function integrity.&lt;/strong&gt; If you replace &lt;code&gt;HTMLCanvasElement.prototype.toDataURL&lt;/code&gt; with your own function, the replacement's &lt;code&gt;toString()&lt;/code&gt; no longer produces the native code string, its &lt;code&gt;name&lt;/code&gt; may be wrong, its property descriptor differs, and the prototype chain can be walked to compare. There's an entire family of these checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-realm consistency.&lt;/strong&gt; It asks the same questions from the main thread, from a Web Worker, and from an iframe. Each of those is a fresh set of native objects. An override that lives in one realm is absent in the others, and the disagreement is the finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Derived-value coherence.&lt;/strong&gt; Fonts, screen metrics, &lt;code&gt;Intl&lt;/code&gt; output, math implementations, audio and canvas rendering are all products of the machine underneath. It cross-checks the story they collectively tell against the one your UA claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lie tallies.&lt;/strong&gt; It doesn't just fail you; it counts how many distinct lies it caught and shows them by name. That's the most useful debugging surface in this entire field, and it's the reason I recommend the page even to people who never ship a spoofed browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The consequence is brutal and clarifying: &lt;strong&gt;every technique based on patching JavaScript from userland is visible to this page.&lt;/strong&gt; Not "might be visible with effort." Visible by default, by design. If your approach is an init script that redefines getters before page scripts run, CreepJS will list it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So the work moved into the engine
&lt;/h2&gt;

&lt;p&gt;That's why the version that passes doesn't inject anything. The values are produced by the browser build itself, from a per-profile config file the process reads at startup. When a page asks for the GPU renderer string, nothing intercepts the call and rewrites the answer — the answer &lt;em&gt;is&lt;/em&gt; the configured value, at the layer that would normally read the driver. There's no patched function to inspect, no descriptor mismatch, and worker threads and iframes report the same thing as the main thread because they're all asking the same underlying implementation.&lt;/p&gt;

&lt;p&gt;The things that made the biggest difference, roughly in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Making the persona self-consistent before caring about any individual value.&lt;/strong&gt; A profile gets one generated identity — OS, Chrome major version, GPU vendor/renderer pair, screen geometry, device pixel ratio, language list, timezone — and every surface derives from that one record. Values invented independently are how you get an NVIDIA renderer on a persona with an Intel-only screen resolution, or &lt;code&gt;Intl&lt;/code&gt; reporting a timezone that contradicts the UA's platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deriving network-adjacent values from measurement, not from taste.&lt;/strong&gt; &lt;code&gt;navigator.connection&lt;/code&gt; exposes &lt;code&gt;rtt&lt;/code&gt;, &lt;code&gt;downlink&lt;/code&gt; and &lt;code&gt;effectiveType&lt;/code&gt;. Real Chrome computes all three from one observed round-trip using published thresholds. So I measure the actual round trip through the profile's proxy, round it to a grid, and run it through the same threshold table Chromium uses. The measured value is biased high — it includes DNS, connect and TLS — and I deliberately don't compensate, because a slightly pessimistic &lt;code&gt;effectiveType&lt;/code&gt; is a real value combination, while a hand-tuned one is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deriving locale and timezone from the exit IP.&lt;/strong&gt; Not from the host. Not from a setting the user forgot to change. The browser looks up where its own traffic actually comes out and configures itself to match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Removing automation artifacts that aren't fingerprint surface at all.&lt;/strong&gt; The one I'm most glad I fixed: proxy authentication. The easy implementation is a small extension that answers the auth challenge. It works perfectly, and it leaves an enumerable entry in the browser's extension list — a "Proxy Auth" item in a browser that's supposed to look like a stock consumer install. Now the credentials are handled in the network layer of the engine itself: SOCKS5 sub-negotiation at the socket level, and HTTP proxy auth answered inside the browser's own auth path. No extension, no JavaScript, and the password never reaches a renderer process.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that still catches me
&lt;/h2&gt;

&lt;p&gt;Here's the one I haven't fixed, stated plainly because you'll hit it too if you go down this road.&lt;/p&gt;

&lt;p&gt;On a Windows host with a Chinese system locale, CreepJS can still surface a CJK UI font in the font results — while the persona claims to be a US English machine. Everything on the fingerprint &lt;em&gt;enumeration&lt;/em&gt; side is clean: the font list is what a stock US Windows install would report. The leak comes through CSS system font keywords (&lt;code&gt;menu&lt;/code&gt;, &lt;code&gt;small-caption&lt;/code&gt;, &lt;code&gt;status-bar&lt;/code&gt;). Those resolve through a different code path than the one I replaced, so the values reported are still the host's real UI font, and the page measures the rendering rather than asking for a name.&lt;/p&gt;

&lt;p&gt;Two lessons in that, both generalizable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The spoofing surface and the rendering surface are different surfaces.&lt;/strong&gt; You can win the first one completely and lose on the second, because detectors don't ask "what fonts do you have," they measure how text renders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The fix has to be in the same code path the value comes from.&lt;/strong&gt; I patched something plausible-looking that turned out never to be called on that platform, and I believed it was fixed for weeks because the enumeration output looked right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A second one from a different platform, in the same spirit. On macOS, the browser's internationalization layer takes the locale from the OS, not from the command-line language flag — so a persona that claims US English while the host is set to something else gets a contradictory &lt;code&gt;Intl&lt;/code&gt; result. The workaround is to pass the locale in the form macOS reads. It works, and it costs you: the argument format isn't recognized as a flag by the command-line parser, so it gets treated as a URL and the browser opens a junk tab on every start. The launcher closes that tab after connecting. For a while I looked for a cleaner trick that would avoid it entirely; the one I tried broke session restore in a way that was much worse than a stray tab, and I rolled it back.&lt;/p&gt;

&lt;p&gt;That's the actual texture of this work. Not clever bypasses — a long list of platform-specific paths, each of which will produce a contradiction if you handle it approximately.&lt;/p&gt;

&lt;h2&gt;
  
  
  What passing does and doesn't buy you
&lt;/h2&gt;

&lt;p&gt;The most honest independent test I've seen scored several commercial anti-detect browsers on consistency and found the marketing ranking roughly inverted: two products sold as top-tier landed mid-pack, and a cheaper one topped the table. The author's own caveat is the right one to end on: &lt;strong&gt;CreepJS is an educational open-source project, and passing it is not the same as not getting banned.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Real production defenses layer things CreepJS never touches — TLS and HTTP/2 fingerprints below the JS layer, IP reputation, account history, behavioral models built from the first request. A profile can score beautifully and still get flagged for arriving from a datacenter IP with no history and mouse-free navigation.&lt;/p&gt;

&lt;p&gt;What passing gets you is the elimination of an entire class of instant, cheap disqualification. It means the contradictions are gone, so when something still fails you can go look at the layers that actually decide. That's worth a lot. It just isn't a finish line, and I'd rather say so than sell you the version where it is.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>privacy</category>
      <category>security</category>
      <category>software</category>
    </item>
  </channel>
</rss>
