navigator.plugins is a real platform object (structured-clone probe)
Check id plugins-structured-clone
What an ordinary browser yields
structuredClone(navigator.plugins) throws a DataCloneError.
What a detector infers
The structured-clone algorithm is implemented in C++ and interrogates objects about what they actually are, not what they claim. A genuine PluginArray is a non-serializable platform object, so structuredClone(navigator.plugins) must throw — and specifically must throw a DataCloneError. This check first confirms the gate (structuredClone exists and Object.prototype.toString.call(navigator.plugins) reports [object PluginArray]), reporting N/A otherwise, then requires that a throw occurs and that the error's name is DataCloneError. Only the error name is asserted, never its message, because wording varies by engine and entry point. A plain Array or object impersonating a PluginArray clones cleanly and silently succeeds — no JS construct can fabricate non-serializability — so a successful clone lets a detector infer the plugin list is a fake that merely carries the right toStringTag, and a differently-named error indicates a JS throw standing in for the serializer's.
How to resolve it
Don't synthesize navigator.plugins from a JS array or object. The serializer's refusal is a hard C++ invariant that a JS replacement cannot reproduce — matching Symbol.toStringTag gets past shallow type checks but not this one. If the plugin list needs to differ, change it at the engine level so the object remains a real PluginArray, or leave the native list intact.
Read in the wild by
CreepJS
CreepJS, a research demo rather than a production detector, flags a fabricated plugin list by inspecting the entries themselves: it requires each plugin's first item to have a prototype whose constructor is named "MimeType", recording a "missing mimetype" lie against Navigator.plugins when it isn't — and treating a throw there as a sign of tampering. It reaches the same signal this check probes, a plugins list synthesized in JS, by prototype inspection rather than by structured clone.
src/lies/index.ts:870-884 (getPluginLies): `// 3. Expect MimeType object in plugins` … `const validMimeType = Object.getPrototypeOf(plugin[0]).constructor.name == 'MimeType'` … `lies.push('missing mimetype')`, with `catch { … return true // sign of tampering }`; recorded as a lie via src/navigator/index.ts:253-258: `const { lies } = getPluginLies(plugins, navigator.mimeTypes)` → `lied = true` → `documentLie('Navigator.plugins', lie)`.
Every attribution traces to a published artifact. See the sources and their limits.
Nearby checks in Environment & locale
- proprietary codecs (H.264 / AAC) present for a Chrome UA
codec-supportThe check calls canPlayType on a video and audio element for H.264 ('avc1.42E01E') and AAC ('mp4a.40.2'), plus MP4/WebM/Ogg for context, and… - Blink-only Web APIs present for a Chrome UA
blink-web-apisWhen the UA matches Chrome/\d+ and the page is a secure context, this probes four Blink-only surfaces: navigator.connection… - CSS feature support matches the engine (Blink for a Chrome UA)
css-engine-surfaceFor any UA containing 'Chrome/', this requires CSS.supports('-webkit-app-region', 'drag') to be true and the Gecko markers — MozAppearance… - every Intl constructor resolves the same locale
intl-locale-coherenceEvery Intl constructor — NumberFormat, DateTimeFormat, PluralRules, Collator — resolves its locale through the same underlying ICU data, so… - canPlayType answers identically for <video>, <audio> and the prototype
canplaytype-element-agnosticcanPlayType is implemented exactly once, on HTMLMediaElement.prototype, so <video> and <audio> inherit the same C++ function. - canPlayType behaves like a real codec parser (bogus + quoting)
canplaytype-parser-trapsA real canPlayType is an RFC-6381 parser, not a dictionary. This check exercises two properties only a parser has. - AudioContext invariants + a deterministic offline render
audio-context-coherenceThe check constructs a live AudioContext and reads four invariants — sampleRate, baseLatency, the destination node's channelCount against… - the analyser byte spectrum is the defined quantisation of its float spectrum
audio-analyser-float-vs-byteAn AnalyserNode publishes the same spectrum twice. getFloatFrequencyData returns decibels, and getByteFrequencyData returns that spectrum…
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 Environment & locale 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 Environment & locale — the family plugins-structured-clone belongs to.