window.chrome present for a Chrome UA
Check id chrome-object
What an ordinary browser yields
A Chrome-claiming UA is accompanied by a present, truthy window.chrome; non-Chrome engines are not scored.
What a detector infers
This is a cross-check, not a probe of window.chrome's contents: it first decides whether the browser claims to be Chrome (a Chrome/ token in the user agent, or the presence of navigator.userAgentData), and only then requires that window.chrome exists and is truthy. Real Chrome always populates this object; older headless builds and stripped or minimally-shimmed environments did not, so a Chrome user agent arriving without it is two signals disagreeing about what the browser is. The N/A branch is what keeps the check honest — on a non-Chrome engine the absence of window.chrome is correct, so nothing is scored. Severity is warn rather than critical because modern headless populates the object and the tell is largely historical.
How to resolve it
Run a real Chromium build so the object is present natively, or stop claiming a Chrome UA on an engine that isn't Chrome. Hand-building a window.chrome does satisfy this check, but it only relocates the problem: as soon as such a shim also fills in chrome.csi or chrome.loadTimes in JavaScript, the native-stubs check catches those members by their toString output. An empty stand-in object clears both checks while still contradicting every other Chrome-shaped signal on the page, which is why the object's presence is scored as a warning rather than treated as proof of a real Chrome.
Read in the wild by
PerimeterX / HUMAN
Reads window.chrome and ships three separate fields: the object's own key list verbatim via Object.keys, a hash of a structural walk of the object and its prototype chain, and chrome.runtime.id as its own plaintext field.
main.min.js (PerimeterX collector, ifood): `Ax=hS[jb("Y2hyb21l")]` + `function AP(){return Ax}` capture window.chrome; in AR(t): `i=AP(),i&&(t["Q3M2OQUTNAM="]=kn(ko(i)))` where ko walks the object and its prototype and kn hashes; in Ci(t): `t[hQ(749)]=...?Object[hQ(386)](hS.chrome):[]` and `t[hQ(746)]=hS.chrome&&hS[hQ(747)][hQ(748)]&&hS[hQ(747)].runtime.id||""` (hQ(746)="NABBSnFnSnk=", hQ(747)="chrome", hQ(748)="runtime", hQ(749)="YjIXOCRfHQs=", hQ(386)="keys"). Captured payloads (sample/4, sample/5 decoded_payload_2.json): YjIXOCRfHQs= ["loadTimes","csi","app"], Q3M2OQUTNAM= "49e5084e", NABBSnFnSnk= "".
CreepJS
CreepJS, an open-source research demo rather than a production detector, counts a Blink runtime with no window.chrome as one of its "like headless" indicators.
src/headless/index.ts line 32, in the `likeHeadless` set: `noChrome: IS_BLINK && !('chrome' in window)`
Kasada
Probes for the window.chrome surface.
window_nav_info_no_chrome, iframe_nav_info_no_chrome
ips.js 427-probe teardown (emro.cat, Apr 2026)
DataDome
Ships window.chrome's own keys as a single field.
O1xeSh = ",loadTimes,csi,app"
glizzykingdreko/datadome-encryption — tests/original.jsonEvery attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Automation surface
- No Puppeteer / Playwright sourceURL marker
sourceurl-leakPuppeteer and Playwright evaluation labels can appear in main-world Error stacks. - No controller code crossed the main-world canary
main-world-executionAn early DOM canary records main-world calls, but page code and extensions can also call it, so this is contextual. - No exposed Puppeteer / Playwright binding
exposed-binding-leaksExposed functions leave Playwright/Puppeteer registries, prefixes, source text, or __installed markers. - Content Security Policy is enforced
csp-bypassThe audit excludes data: scripts. Execution despite script-src is direct CSP-bypass behavior. - window.chrome.* helpers are native (not JS stubs)
chrome-native-stubsWhere the previous check asks whether window.chrome exists, this one asks whether its members are genuine. - no notifications-permission headless bug
permissions-notification-bugTwo independent APIs describe the same underlying notification permission: navigator.permissions.query({name:'notifications'}) returns a… - permissions.query is native (arity throw + stack shape)
permissions-query-stackThis probe calls navigator.permissions.query() with exactly zero arguments and inspects the error the native arity check produces. - timer handles are a real per-realm counter
timer-id-sequencingThe number setTimeout hands back is not a token, it is an index into the realm's own timer table — so it carries structure that a fabricated…
Clearcote is a browser built for fingerprint coherence
It is a Chromium fork, maintained by the same people who wrote this reference. It ships as a compiled browser rather than as a stealth script injected into someone else's — which is a description of how it is built, and is not an argument about how it behaves on this check.
This audit takes no position on how Clearcote scores on Automation surface checks, on this one, or anywhere else. It has no baseline corpus of other people's fingerprints to rank you against and no vendor scoreboard — nearly every check is self-referential, asking one browser the same question through two independent APIs and reporting whether both answers can be true at once. It runs identically on any browser, including ours. Run it on yours and read the result yourself.
See the other checks in Automation surface — the family chrome-object belongs to.