BreakHub
GuidesBrowser fingerprinting

User-Agent decay and the rise of Client Hints

The User-Agent string carries thirty years of compatibility baggage, and Chrome has frozen it into a near-empty template. Client Hints took over the details. Here is what each one exposes, and how sites catch a browser whose UA has been edited.

6 min read · Published September 10, 2026

Open any Chrome and print navigator.userAgent. You get a string that contradicts itself: it starts with Mozilla/5.0, mentions AppleWebKit and "KHTML, like Gecko" in the middle, and ends with Safari/537.36. A Chrome claiming to be a relative of Netscape, Safari and Firefox at the same time. That is not a bug. It is a fossil built up from decades of compatibility hacks. How it got that way, why it is now being frozen, and what sites use instead is the background behind the browser section of our fingerprint report.

A string nobody trusts

The original idea behind User-Agent was modest: tell the server what software is asking, so it can serve something appropriate. Things went wrong in the nineties when sites started gating their "advanced" pages on whether the UA contained "Mozilla". Internet Explorer promptly called itself Mozilla, and every later browser copied the trick. When Chrome launched, it wanted every site that sniffed for "Safari" or "Gecko" to keep working, so it stuffed all of those tokens in too. The result is a string where nearly every token is a lie and the only honest parts are the trailing Chrome/1xx and the platform bit in parentheses.

Worse, the UA also carried the OS version, CPU architecture and, on Android, the exact device model. Almost no site needs any of that, but every tracker gets it for free.

Chrome's UA reduction

Roughly between 2022 and 2023, Chrome shrank the UA string in stages. A current Chrome UA looks approximately like this: Windows always reports Windows NT 10.0; Win64; x64, whether you run Windows 10 or 11; macOS is pinned to Intel Mac OS X 10_15_7 even on Apple Silicon; Android is frozen at Android 10 and the device model is replaced by the single letter K; and the Chrome version keeps only its major number, so you see something like Chrome/128.0.0.0.

So a Chrome UA today tells you three things and not much more: the rough platform, whether it is mobile, and the major Chrome version. That was the point. Reduce the UA to low entropy, then move the details into a mechanism that has to be asked for.

Client Hints: what is sent by default versus on request

Client Hints (UA-CH) is that mechanism, and it splits browser information into two tiers.

Three low-entropy hints go out with every request: Sec-CH-UA (brands and major versions, e.g. "Chromium";v="128", "Google Chrome";v="128"), Sec-CH-UA-Mobile (?0 or ?1) and Sec-CH-UA-Platform ("Windows", "macOS", "Android" and so on). Together they reveal about as much as the reduced UA, by design.

The high-entropy hints are not sent unless requested: Sec-CH-UA-Platform-Version (which distinguishes Windows 10 from 11), Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Model (the real device model), Sec-CH-UA-Full-Version-List (the full version numbers) and Sec-CH-UA-WoW64. A server obtains them by answering with an Accept-CH: Sec-CH-UA-Platform-Version, Sec-CH-UA-Model response header, after which the browser attaches them to subsequent requests. Page script can also call navigator.userAgentData.getHighEntropyValues(['platformVersion', 'model']) and get them back in a Promise.

One misunderstanding is worth clearing up here. "Requires permission" does not mean a prompt you click. It is declarative: the site says it wants the values and the browser hands them over without asking you. What you gain is that they are absent by default. A passive request log contains none of them; anyone who wants them has to ask explicitly, and explicit asking can be audited and restricted by policy. Also note that, as of writing, neither Firefox nor Safari implements UA-CH, and neither has frozen its UA as aggressively as Chrome. "Client Hints replace the UA" is, for now, mostly a Chromium story.

That odd "Not A(Brand" entry

Look at Sec-CH-UA and you will find something that looks like corruption among the brands, such as "Not A(Brand";v="99" or "Not/A)Brand";v="8". This is GREASE, deliberate noise. The motivation is practical. If a developer writes if (brands[0] === "Chromium"), any future brand or reordering breaks their site, exactly as "Mozilla" sniffing did decades ago. So Chrome injects a made-up brand at a random position with a nonsense version, forcing everyone to write a tolerant parser from day one.

Engine detection: what the UA claims matters less than how the browser behaves

Changing a UA is trivial. An extension, a proxy, or a settings page in any anti-detect browser will do it in one click. Serious risk systems therefore ignore what the UA says and look at what the browser does. That is engine detection.

A few families of probes are typical:

  • window.chrome exists only in Chromium-based browsers. Older Firefox exposed InstallTrigger (removed in recent versions, so that probe is fading). Safari has WebKit-only objects like ApplePayError and safari.pushNotification.
  • CSS feature support. CSS.supports('-moz-appearance', 'none') being true almost always means Gecko; -webkit- properties exist in both Blink and WebKit, but the exact sets differ.
  • Whether navigator.userAgentData exists at all. A browser claiming to be Chrome 128 without that object is either Firefox wearing a Chrome UA or has had it stripped by an extension.
  • Error stack format. V8, SpiderMonkey and JavaScriptCore produce visibly different Error().stack text, and no UA edit changes that.

Once the engine is known, the next step is to estimate the real version. The method is essentially the Chrome Platform Status table run backwards: every Chromium release adds documented APIs. One release introduced Array.prototype.toSorted, another added a scheduling API, another a new CSS selector. Probing for these features one by one boxes the true Chromium version into a narrow range. If the UA claims Chrome 128 but APIs that shipped in Chrome 115 are missing, the UA has been edited.

Our User-Agent page also does something simpler: it puts the UA from the HTTP request header next to navigator.userAgent from JavaScript. Normally they are identical. If they differ, there are essentially two explanations, a proxy rewriting headers in the middle or an extension tampering with the JS side. This is the heaviest deduction in our authenticity score, because it has almost no false positives.

What inconsistency costs you

Editing the UA without editing the engine has more consequences than people expect, and most of them are bad for you.

Risk engines treat a "claimed UA versus measured features" mismatch as a strong signal of a faked environment, which means more CAPTCHAs at best and a flagged account at worst. Compatibility logic takes the wrong branch: claim a newer version and the site ships modern syntax your older engine cannot parse, giving you a blank page; claim an older one and you get the degraded experience. There is a security angle too. Update nags and version-gated security features key off the UA version, so misreporting it silently switches those reminders off.

A UA edit is the easiest disguise to apply and the easiest to see through. Either make the whole environment consistent or leave it alone.

FAQ

Are Client Hints worse for privacy than the UA? The high-entropy hints do reveal more than the reduced UA, such as your actual Windows version and device model. But those used to be given away in the UA for free; now they at least have to be requested, and only Chromium-based browsers provide them. The shift is from "visible to everyone" to "available on request". That is an improvement, but do not mistake it for a privacy feature.

Did Firefox and Safari freeze their UAs too? Not to Chrome's extent. Both have trimmed some details over the years, but neither implements UA-CH, so their UA strings remain their main identifier.

Can a UA-switching extension make a site think I use another browser? Only sites that read nothing but the UA. Feature probes see through it, and once caught, "UA does not match engine" is a worse signal than the original UA. If the goal is less tracking, a default browser that looks like everyone else's beats any disguise.