The "Not A Brand" entry in Chrome's user agent is not random and not a bug. It is a GREASE brand, a deliberately meaningless entry that Chrome adds to navigator.userAgentData.brands and the Sec-CH-UA header, and Chrome computes it from its own major version with a fixed formula. Every Chrome of one major sends the same label, the same version and the same order, which means a site can work out what that entry should be and check it.

GREASE stands for "generate random extensions and sustain extensibility". The idea comes from TLS: if clients always send a few junk values, servers that choke on anything unexpected get found and fixed early. The User-Agent Client Hints proposal asks browsers to do the same with brands, so that no site writes if (brands.length === 2) and breaks the day a third one appears.

How Chrome computes the Not A Brand entry in its user agent

The code lives in Chromium's user agent utilities, and the patch our engine carries for the user agent uses the same tables. There are three inputs, and all of them come from the major version, which Chromium uses as the seed:

characters   " "  "("  ":"  "-"  "."  "/"  ")"  ";"  "="  "?"  "_"
versions     "8"  "99"  "24"

label    = "Not" + characters[major % 11] + "A" + characters[(major + 1) % 11] + "Brand"
version  = versions[major % 3]
order    = one of the 6 permutations of the three brands, picked by major % 6

In the full version list (fullVersionList in JavaScript, Sec-CH-UA-Full-Version-List on the wire) the GREASE version gets .0.0.0 appended, so an 8 becomes 8.0.0.0. The real brands carry the real build number there.

Worked through for a few majors:

Majormajor % 11LabelVersion
1441Not(A:Brand8
1529Not?A_Brand24
15310Not_A Brand8
1540Not A(Brand99
1551Not(A:Brand24

The table has eleven characters and the versions repeat every three, so the same label comes back every eleven releases but with a different version beside it. That is why 144 and 155 share Not(A:Brand and still differ.

Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Machines that exist

We checked it twice, because it looked wrong both times

While we were chasing a detector verdict in September 2026, our engine was on major 153 and the Google Chrome on the same Mac was on 152. Ours read Not_A Brand version 8. Stock read Not?A_Brand version 24. Two browsers that should match, with a different brand in the list, looks exactly like the cause you are hunting for.

It was arithmetic. 153 modulo 11 is 10, which is _; 154 modulo 11 is 0, which is a space; 153 modulo 3 is 0, which is 8. The stock result works out the same way for 152. Read through the app on a real profile, the whole set came out as Chrome computes it:

brands           Google Chrome 153, Not_A Brand 8, Chromium 153
fullVersionList  Google Chrome 153.0.8010.37, Not_A Brand 8.0.0.0, Chromium 153.0.8010.37

The order checks out too. 153 modulo 6 is 3, and that permutation puts Google Chrome first and Chromium last, with the GREASE entry between them.

The second time was a bare launch of the engine binary without the switches Mosaic passes, used to start a bisect. It reported Not(A:Brand version 8 next to a Chromium 144 left in an old table. The brand matched the formula for 144, so the GREASE entry was consistent with the stale version. The stale table was the bug, and it sat on a path no profile takes: the launcher always passes the switches that replace it.

What a site can check

None of this identifies a person. What it does is give a site a free consistency test, because the right answer can be derived from the version the browser claims. A browser fails it in a few distinct ways:

  • The label belongs to a different major. A spoof that copied a brand list from one Chrome and bumped the version numbers keeps the old punctuation.
  • The version is not 8, 99 or 24, or is the wrong one of the three for that major.
  • The three entries are in an order Chrome would not produce for that major, or the list has two entries where branded Chrome has three.
  • The full version list gives the GREASE entry a real build number, or gives the real brands none.
  • The page says one thing and a service worker says another, because the spoof lived in page JavaScript and the worker never ran it.

The fourth item has a sibling we got wrong ourselves. Our engine was once sent the major alone where it needed the full version, so uaFullVersion read 153 where a stock Chrome reads something like 152.0.7977.83. No Chrome has ever reported a full version with no build number. Once the launcher sent all four parts, FingerprintJS's confidence moved from 1 to 0.99, which was the exact figure stock Chrome got. A field nobody looks at was being read.

How Mosaic gets it right

Tesseract Chrome, our own Chromium, computes the brand list in C++ from the version the profile claims, using Chrome's own tables. The page, every frame, every worker and the Sec-CH-UA header are all built from that one list, so there is no second copy to disagree with the first. Nothing is overridden in JavaScript, which is the reason in why we change fingerprints in C++. Mosaic also refuses to claim Edge, because one switch feeds both the client-hint versions and the User-Agent suffix and Edge needs them in two different shapes.

If you want to check a browser yourself, read navigator.userAgentData.getHighEntropyValues(['fullVersionList']) in its console, take the major, and run the formula above. The label, the version and the position should all come out the same. When they do and a detector still complains, the next suspect is branding rather than the brand list: the FingerprintJS demo that could not name our browser turned out to be about Google's branded build, and running Google's own unbranded build as the control is how we told the two apart. The other values that make up a browser fingerprint are worth the same kind of check.