How the probe works
The standard probe uses OfflineAudioContext — a Web Audio API that renders samples without actually playing them, so the user hears nothing. The script creates an oscillator at a known frequency (typically 1000 Hz), routes it through a DynamicsCompressorNode or BiquadFilterNode with known parameters, renders ~5000 samples, and hashes them with SHA-256.
The hash is stable per (browser, OS, CPU architecture, audio driver) and varies between them. Two iPhones produce the same hash; an iPhone and an Android produce different hashes; a real macOS user and a headless Chrome on the same machine produce different hashes because the audio fallback path is different.
The headless tell
Headless Chrome on a server with no audio device falls back to a stub audio backend. The stub produces a small set of known hashes — roughly three distinct values across the entire population of headless Chrome instances on Linux servers. Anti-bot vendors maintain a blocklist of these specific hashes and flag any request matching them.
Even with --use-fake-device-for-media-stream and similar Chrome flags, the OfflineAudioContext path is independent of media-device flags. The fix is one of: (1) run on a machine with a real audio device passed through to the browser, (2) install a virtual audio driver (PulseAudio dummy sink, BlackHole on macOS) that produces real-machine-like output, or (3) use a tool that patches AudioContext at the engine level (Camoufox, CloakBrowser).
Why naive spoofing fails
Spoofing AudioContext in JavaScript is detectable in two ways. First, Function.prototype.toString() on the patched method reveals the patch — real Chrome's AudioContext.prototype.createOscillator.toString() returns "function createOscillator() { [native code] }"; a JS replacement returns the patch source. Second, the timing of OfflineAudioContext.startRendering() on a real audio stack vs a JS-stubbed one differs by orders of magnitude, and that timing is itself fingerprinted.
The only reliable mitigation is patching at the browser-engine level (below the JS layer) so toString() still returns [native code] and the render timing matches a real machine. This is what differentiates Camoufox / PatchRight from playwright-extra-plugin-stealth.
