Skip to content

Browser signals

phil/bot-detection-bundle

The browser code is a Stimulus controller, verdict (assets/src/verdict_controller.js), and its detectors (assets/src/detectors.js), written from scratch. It lets one word out of the browser: human or robot.

PieceState
The browser detectorsImplemented, tested with Node; no third-party code included
The wire format (human or robot)Decided, and read by Verdict::fromSubmitted() on the server
The server side (verdict, friction, counter)Implemented and tested

Placing the controller

With StimulusBundle, the controller is found from the bundle's assets/package.json. On the root element, it leaves the verdict as data-phil-bot-verdict, where phil/analytics-bundle's collection script reads it; on a hidden field, it also writes the verdict into the field, for the form to submit:

<html data-controller="phil--bot-detection-bundle--verdict">
…
<input type="hidden" name="verdict" data-controller="phil--bot-detection-bundle--verdict">

Signals, not a fingerprint

The browser code evaluates independent automation signals, each answering one question ("is this browser driven by automation?"), and combines them into a single verdict on the spot. A fingerprint answers another question ("which browser is this?") and needs a stable combination of many attributes; nothing here is designed to answer it, and nothing that would answer it is computed, hashed or sent.

The detectors

Each detector answers true (automation), false, or undefined when it cannot tell; one true makes the verdict robot. The detectors written are:

DetectorReadsAutomation when
webdrivernavigator.webdriverit is true
automationTracesthe window's and the document's propertiesa Selenium, PhantomJS, Nightmare or WebDriver global is present, or a ChromeDriver cdc_ property
nodeRuntimewindow.process.versions.nodethe page runs in Node.js
headlessChromethe user agent and the user agent hints' brandseither names HeadlessChrome
noLanguagesnavigator.languagesit is an empty list
zeroSizeWindowwindow.outerWidth, window.outerHeightboth are 0
platformMismatchnavigator.userAgentData.platform against navigator.platformWindows, macOS and Linux disagree
automationStackthe stack of an Errora Puppeteer, PhantomJS, Selenium, Playwright or WebDriver frame
softwareRendererthe WebGL renderer, when the browser tells itSwiftShader, llvmpipe, softpipe or a software rasterizer

The signals of the design not written yet — rewritten native functions, the window against a worker, permissions, speech voices, WebRTC, hardware and screen consistency — are listed below as the full design.

SignalWhat it detects
navigator.webdriverA browser under WebDriver control
Globals specific to automation engines (Selenium, PhantomJS, Nightmare)Properties those engines inject into the page
Automation traces in stack tracesFrames that only automation frameworks produce
An exposed process objectA page running inside a Node.js-based runtime
Inconsistencies between the user agent and the platformA user agent that claims what the platform contradicts (platform against userAgentData)
Inconsistencies of languages, plugins, MIME types and permissionsValues a real browser would not combine
Rewritten or proxied native functionsNative APIs patched to hide automation
Comparing the window with a workerValues spoofed in the window but not in a worker
Headless Chrome hintsTraits specific to headless mode
A software WebGL renderer, WebGL vendor and rendererRendering without a GPU, typical of headless servers
A zero-size windowA window no one looks at
Secondary consistency signals: permissions, speech-synthesis voices, WebRTC, hardware (hardwareConcurrency), screenInputs to consistency checks only

Each signal is read locally and dropped. Only the verdict derived from them leaves the browser, with the request it decides.

What is deliberately never collected or sent

  • No visitor identifier and no fingerprint: every hash and visitor-id computation found in the design references is left out.
  • No raw signal and no detector detail: the server receives human or robot, nothing else. A longer payload is read as Unknown (verdict.md).
  • No call to a third party: none of the "monitoring" or sampling calls the references make on their own (to a vendor endpoint, or loading a remote script) is reproduced. The script is served by the application, from its own origin.
  • No storage: no cookie, no local storage, no persistent state in the browser.

A browser that resists fingerprinting is not a robot

Privacy-focused browsers randomize or blank some of the very attributes listed above. A detector must not treat randomized or blanked signals as automation. This is a requirement on each detector, not on the server policy: by the time a verdict reaches the server, it is already robot or human.

Design references

The detectors are written from scratch. The projects below are read detector by detector to understand what a signal measures, and only what serves a robot-or-human verdict inspires a detector. No line of their code is copied. A source that is not open source is not read at all: rewriting code under a licence that forbids redistribution would make a derivative work. Licences are those of the files read on 2026-10-30.

ReferenceLicence readVersion readWhat inspires a detectorLeft out
BotD (fingerprintjs/BotD)MITv2.0.0The main reference: a { bot, botKind } verdict without an identifier, and independent detectors with their tests (webdriver, automation globals, stack traces, language/plugin/MIME/permission inconsistencies, exposed process, software WebGL renderer, zero-size window)monitor(), which calls a vendor endpoint; telling good bots from bad, which is done server-side with a crawler list
CreepJS (abrahamjuliot/creepjs)MIT; its name is a trademark, so it is never used as the name or brand of this bundle (a factual mention of origin is allowed)No release; last commit read 2026-06-11What BotD covers poorly: rewritten or proxied native functions, window versus worker comparison, headless Chrome hintsThe fingerprinting part and the prediction module
ThumbmarkJS (thumbmarkjs/thumbmarkjs)MITv1.12.0Secondary consistency signals: permissions, speech-synthesis voices, WebRTC, hardwareHashing, sampled logging to a vendor endpoint, and the remote script, whose licence is not read
FingerprintJS (fingerprintjs/fingerprintjs)MIT up to v3.4.2 and from v5.0.0; Business Source License 1.1 from v4.0.0 to v4.6.2v5.3.0Robust collection techniques as consistency inputs: platform against userAgentData, plugins, WebGL vendor and renderer, hardwareConcurrency, screenAll v4 code, which is not open source and is not read; the visitorId and all hashing; the monitoring call

Never read: Benny the Doorman (npm package doorman-benny), under a "Source-Available Non-Redistribution License" that forbids redistribution, even modified.

The detectors were written on 2026-11-04 from what each signal measures, after reading these references.

Server-side counterpart

Bots that declare themselves (crawlers announcing their name in the user agent) are not a browser matter: they are recognized on the server from the user agent, with a crawler list such as jaybizzle/crawler-detect, outside this bundle.