A TLS fingerprint mismatch with the User-Agent is a header naming one browser over a handshake made by another. A Python script that sends Chrome/153.0.0.0 from a ClientHello built by requests is the common case: the server has the handshake before it has the header, and before any page script runs. Our engine avoids it by doing nothing to the handshake. Chromium makes it, we send it as made, and no profile may claim a Chrome version or brand that handshake does not belong to.
Where a TLS fingerprint mismatch with the User-Agent comes from
A site learns who you are three times per connection, and a different part of the browser writes each one.
- The TLS ClientHello: versions, cipher suites, extensions, groups, ALPN. JA3 and JA4 are hashes of it. In Chrome it comes from BoringSSL.
- The HTTP/2 connection preface: the SETTINGS frame with its values and their order, the window update, the pseudo-header order. Akamai's HTTP/2 fingerprint reads these, and we went through it field by field.
- The headers:
User-Agent, theSec-CH-UAclient hints,Accept-Language, and their order.
A setting can reach only the third. The network stack writes the first two. Nearly every mismatch is therefore one move: someone edited layer three and left one and two describing the real client. A User-Agent switcher extension does it. An HTTP library with a pasted Chrome string does it. A proxy that decrypts HTTPS does it too, because the site then reads the proxy's handshake while the browser's headers pass through.
The fingerprint is changed inside the engine, not painted on topIn Mosaic: Its own Chromium. Tesseract Chrome: values changed in C++, not JavaScript. 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.
How it works: Its own Chromium
Why the handshake is inherited, not drawn
All copies of one Chrome version send the same ClientHello shape. Give each profile its own TLS persona and you do not get two ordinary Chrome users; you get one plausible Chrome and one browser that nobody else on the internet runs. Tesseract Chrome is stock Chromium source plus a patch series, and no patch in it touches BoringSSL or the HTTP/2 code. A profile sends what Chromium sends.
That was an argument until we measured it, back when the engine was on Chrome 153. One profile against Google's own Chrome for Testing of the same build, on one machine, a minute apart:
ours JA4 t13d1518h2_8daaf6152771_4980c97edce0
stock JA4 t13d1518h2_8daaf6152771_4980c97edce0
Fifteen cipher suites, 18 extensions, ALPN h2, and both hashes equal. The JA3 strings were 995f48479d09 and 624b4feda192. They differ because Chrome shuffles its extension order on every connection and JA3 hashes the order, so two runs of one binary disagree on JA3 as well; we compare extensions as a set for that reason. The shuffle and the GREASE values come through untouched, which leaves a detector no fixed JA3 to learn.
The version is the part that drifts
Agreement needs a matching claim as well as a real handshake, and the claim is where an anti-detect browser can break things without help from anyone.
In the same round, Google Chrome 152 on the same Mac read t13d1517h2: 17 extensions where 153 sent 18. The TLS stack changes between majors. A profile whose User-Agent says 152 on an engine that is 153 sends a genuine Chrome handshake from the wrong Chrome, and a detector does not have to look hard to see it. JA4 moves with the Chrome major has the full comparison.
So we tie the claim to the engine. The generator writes the User-Agent as Chrome/<major>.0.0.0 from the profile's major, and the full build number in the client hints is taken from the engine when the two majors agree. At launch the claimed major goes to the engine resolver, so an engine older than the profile is never chosen while a newer one is installed. If the old engine is the only one on the machine, the profile launches anyway and its audit reports that the versions disagree. Refusing would leave the profile unusable; passing in silence would hide the drift.
The brand drifted once, in the headers
We broke the rule ourselves, one layer up. For a time, 14% of Windows profiles were drawn as Microsoft Edge. On the engine we shipped then, one profile launched twice, with a local server reading the User-Agent and client hints off the wire:
without --user-agent UA Edg/153.0.8010.37 Sec-CH-UA-Bitness "64"
with --user-agent UA Edg/153.0.0.0 Sec-CH-UA-Bitness ""
No real browser sends either row. User-Agent reduction zeroes everything after the major, so an Edge User-Agent never carries a full build number, and nothing sends an empty bitness next to a filled-in architecture hint. The cause was one switch feeding two places: the client-hint version lists, which need the full build, and the User-Agent suffix, which needs the reduced form. And the row a launch got depended on a cache that was empty at every app start, so the first profile of a session wore one identity and the later ones wore the other.
That contradiction sat between two headers, not between a header and the handshake. The fault is the same one: a claim the engine could not spell. The engine refuses Edge now, every profile claims Chrome, and check:brand fails the build if the generator ever draws Edge again.
HTTP/2 follows the same rule
The SETTINGS frame, the initial window update and the pseudo-header order come out of the same network stack as the handshake, and we leave them alone for the same reason. An HTTP CONNECT tunnel or a SOCKS5 proxy keeps all of it, because the proxy moves the encrypted bytes without opening them. Header order through a tunnel covers the headers. There is one exception, on purpose: Settings › Network can route launches through Proxyman, Charles, mitmproxy, Fiddler or Burp for debugging, and while it is on, sites read the inspector's TLS, not Chrome's.
How to check your own
Open a JA3/JA4 echo page twice on one machine, once in the browser under test and once in a stock Google Chrome of the same major. Compare the JA4, not the JA3. Then check that the Chrome/ major in the User-Agent equals the version chrome://version shows. A JA4 equal to stock Chrome's and a major equal to the engine's means the three layers agree. Our detection catalogue has a JA3/JA4 echo row for this check, and why our engine has no TLS fingerprint setting gives the reasons there is no per-profile switch.
We would revisit this for a detector that scores a handshake we send worse than stock Chrome's. So far we have not found one.



