Emulation.setUserAgentOverride called without userAgentMetadata changes the User-Agent string and clears the client hints for that page. Called with them, it hands the page a brand list that must match the GREASE entry Chrome itself computes, which the browser's other realms still report. Either way the override reaches one target and not the workers. We stopped sending it, and the User-Agent is set at launch, where every context agrees.
What setUserAgentOverride takes
The command lives in the DevTools protocol's Emulation domain, with an older twin in Network. Automation libraries' set-user-agent helpers send it underneath. It takes four things: userAgent, the string; acceptLanguage; platform, which becomes navigator.platform; and userAgentMetadata, the structure that feeds navigator.userAgentData and the client hints. When we still sent it, our call looked like this:
Emulation.setUserAgentOverride {
userAgent, acceptLanguage, platform,
userAgentMetadata: {
brands, fullVersionList, fullVersion,
platform, platformVersion, architecture,
model, mobile, bitness, wow64
}
}
Ten fields in the metadata, and every one of them is something a page can ask for. getHighEntropyValues() returns most of them on request, and the matching Sec-CH-UA-* headers carry the same values to the server (the reading side is in getHighEntropyValues, explained).
Called without userAgentMetadata
The metadata is optional, and leaving it out looks harmless. Chromium does not treat it that way. Given a User-Agent string it did not build and nothing to describe it, it will not guess the hints from the string, so it empties them.
We measured that on a profile: the page's navigator.userAgentData came back with empty brands and an empty platform, while a worker in the same browser reported both. No real Chrome has a page with no brands. And the contrast with the worker is a second finding on top of the first, from one script that reads two realms.
Everything the window does, from a script on your own computerIn Mosaic: REST API. Everything the window does, from your own scripts. Free holds 3 profiles. Pro is $49 a month for any number.
Mosaic is a desktop app
Email yourself the link and open it on your computer.
Called with userAgentMetadata
Supplying the metadata fixes the empty fields and opens a subtler problem. The brands list carries a GREASE entry, the Not_A Brand-style decoy that Chrome computes from the major version by a fixed formula: its label, its version and its position in the list. (Not_A Brand has the formula and the values it produces.) Whatever list you hand the override is what the page reports, so your list has to reproduce Chrome's own entry exactly.
The parts of the browser the override never reached keep Chrome's own list. So a hand-built brands array that is off by a label, a version or a position gives the page one GREASE entry and the worker another. That is the GREASE mismatch the metadata route brought back for us.
The override reaches one target
Both cases share the bigger flaw. setUserAgentOverride is sent to a target session, and it applies to that target. A service worker is a separate target, and in our measurements it kept reporting the machine's real User-Agent while the page reported the override. Any page that registers a service worker and compares navigator.userAgent across the two gets the spoof for free.
Reaching into the service worker over the protocol is no rescue. A service worker controls the page it serves, and evaluating into it over the protocol deadlocks that page, which we found the hard way. The rule we took from it is that a value we can change in the page but not in a worker is a contradiction, and a contradiction tells a detector more than the real value would.
The --user-agent flag and its own cost
The launch flag --user-agent reaches every context in the process: the page, dedicated, shared and service workers, and the network stack. That fixes the reach. On a stock Chrome it has a cost of its own, measured with that flag as the only change and a string byte-identical to Chrome's default:
without --user-agent architecture "arm" platformVersion "26.6.2"
with --user-agent architecture "" platformVersion ""
Same rule as the override: Chromium will not vouch for high-entropy hints it cannot derive from a User-Agent it did not choose. So on a stock browser the honest options were a string that did not match the hints, or no flag at all, and our launcher sent the flag only when a profile's User-Agent really differed from the engine's. That decision then turned out to depend on a cache, which is the story in a fingerprint that depended on launch order.
Where Mosaic sets it now
On Tesseract Chrome, Mosaic's own Chromium, none of these three routes is needed for a Chrome profile. The engine builds the User-Agent and the client hints in C++ from the platform and browser version the profile claims, so the string, navigator.userAgentData, every worker's copy of it and the request headers all come from one place. The launcher skips the CDP override on our engine entirely. Two mechanisms setting one value compose into something neither predicted, and that is the shape a consistency scanner looks for (why we change values in C++, not JavaScript).
If you drive a stock Chrome with Puppeteer or Playwright and need a different User-Agent, read the result in three places before trusting it: navigator.userAgentData in the page, the same in a worker, and the Sec-CH-UA headers your own server receives. If the three disagree, a site reading them will see it too.





