TL;DR — Both sides of the bot-vs-human arms race in one repo: a cross-layer fingerprint + behavioral detector (blue) and a fleet of real anti-detect evaders (red), scored against each other. The thesis: catch the contradiction across layers, not the signal. Try it live →
Your privacy: the live site captures nothing — it scores your signals in memory to show you a verdict, then forgets them. Never written to disk, retained, sold, shared, or used to track. Details →
Try the live test → kitsune.id
The live verdict, catching this capture's own headless browser — per-layer bars and the explainable tells.
The two sides run against each other to produce a reproducible, per-layer scoreboard, and the red team keeps the blue team honest: no detection ships until a real evader has exercised it and the calibration gate proves it doesn't flag real browsers. Named after the shapeshifting fox spirit.
Modern anti-detect tools defeat single-signal detection — they patch navigator.webdriver, forge a
Chrome TLS fingerprint, randomize the canvas, spoof the timezone. So Kitsune doesn't grade signals in
isolation. It correlates everything a session emits — across the TLS handshake, the HTTP/2 preface, the
TCP/IP stack, the JS runtime, and behavior — and flags the incoherence between layers that a spoof
can't avoid:
- the TLS handshake says Chrome on Windows, but the TCP/IP stack says Linux;
- the page's
navigatorreports 8 cores, but a Web Worker it spawns reports 12 — the spoof patched the main thread and forgot the second realm; - every field is individually valid and mutually consistent, yet the joint fingerprint is one no real user has (the statistical-improbability frontier);
- one shared TLS identity fans out across 300 residential IPs with per-instance-randomized JS — a coordinated fleet wearing distinct masks.
A real browser is coherent for free. A bot has to reproduce that coherence across every layer simultaneously — and that is much harder than fooling any one of them.
one session_id — threaded through every hop
evader ─▶ EDGE (Go) ─▶ COLLECTOR (TS) ─▶ DETECTOR (Python) ─▶ Verdict
│ │ │
network.* browser.* + coherence engine (rules-as-data)
TLS·JA4·HTTP-2 behavioral.* → noisy-or ⊕ incoherence ⊕ gate
QUIC·TCP/IP·DoS canvas·WebGL │
audio·realm·biomech ▼
STORE ─▶ HARNESS ─▶ per-layer SCOREBOARD
The edge (Go) fingerprints the network layers a UA-spoofer can't reach — it terminates TLS and reads
the raw ClientHello for JA3/JA4 (GREASE-filtered, post-quantum-aware), the JA4+ suite (JA4H for
HTTP/2, JA4T for TCP), the HTTP/2 SETTINGS + frame-order (Akamai) fingerprint, QUIC/HTTP-3 (RFC 9001
ClientHello decrypt), the TCP/IP-OS stack (p0f-style SYN sniff) and HTTP/2 DoS attribution — then
mints a session_id and forwards network.* signals. The collector runs in the
browser and emits browser.* + behavioral.* signals under the same session. The detector groups
them into a session, runs a generic engine over the rules-as-data registry, and emits an explainable
verdict where every point of bot-likelihood traces back to its evidence. Components are polyglot and
never import each other — the JSON-Schema contracts/ are the only coupling.
183 live rules (129 active · 54 experimental; 6 retired, ruleset 0.74.57) — each a small predicate over the correlated session. 137 can convict (coherence/automation/artifact); the rest only corroborate. Grouped by detection class:
| Class | Rules | Convicts? | What it catches |
|---|---|---|---|
| coherence | 85 | ✦ | cross-vector contradictions (TLS↔TCP↔UA↔JS↔h2↔QUIC) — the thesis core |
| automation | 36 | ✦ | the framework surface: webdriver, CDP runtime, Electron, isolated-world leaks |
| artifact | 16 | ✦ | anti-detect implementation flaws: tampered natives, spoof placeholders |
| environment | 26 | — | stripped/headless capability gaps (corroborating only — see precision) |
| behavioral | 14 | — | mouse/keystroke biomechanics — path straightness, velocity CV, entropy floors |
| reputation | 5 | — | datacenter ASN / known proxy exit / WebRTC-leaked origin |
| prevalence | 1 | — | statistically-improbable-but-coherent fingerprints |
✦ convicting · — corroborating-only. The conviction gate means corroborating signals can never reach bot alone.
A distinctive capability is the realm-coherence family: anti-detect tools spoof the main JS realm but
systematically forget other realms. Kitsune compares navigator, timezone, languages, the WebGL
renderer, and the canvas pixel-hash across the main thread vs a Web Worker and an iframe — and a
guard (worker_constructor_tampered) closes the one escalation, since wrapping Worker to spoof the
worker scope makes the constructor non-native. See the detection catalog.
Catching bots is easy; not flagging real people is the hard part. Scoring is a transparent noisy-or
with cross-layer amplification, behind a conviction gate: a bot verdict requires at least one
convicting signal (coherence / automation / artifact). The corroborating classes — environment,
behavioral, reputation, prevalence — raise suspicion but can never convict alone, so a stripped-but-real
browser (no webcam, no plugins) can't noisy-or its way to bot.
The calibration harness is the trusted-but-verified false-positive gate: it scores thousands of real browser fingerprints and measures, per rule, how often each fires on a legitimate browser. It is deliberately multi-source — a generated distribution (browserforge) plus real Chromium/Firefox/WebKit captures — because you must never down-weight a rule on a single source's number. Every new rule is grounded against a real browser before it ships, and a regression test fails the build if any rule starts firing on a real engine.
No rule ships on a hunch. Every detection runs the same red⇄blue loop, and the order is the discipline:
- Red confirms the evasion first. A purpose-built evader mode has to actually defeat the current detector — if nothing evades, there's nothing to detect.
- Blue builds the detection as data in the registry, and it must CONVICT that evader.
- The FP gate has the last word. The rule is scored against thousands of real fingerprints and ships only if it stays clean — and a regression test fails the build if it ever fires on a real engine.
The gate is deliberately multi-source, because you must never trust a single dataset's number: a generated distribution (browserforge) plus real Chromium/Firefox/WebKit captures, cross-checked against Intoli user-agents and fpgen, with the behavioral floors grounded on real human motion — SapiMouse (mouse), BrainRun (161k mobile swipes) and Aalto (keystroke). Mobile and touch get their own grounded floors, not desktop heuristics reused.
What feeds the loop is docs/research-radar.md: an intake queue of external
papers, tools and releases, each mapped to a Kitsune seam and tagged groundable-in-sandbox vs
external-data-bound. Groundable leads become rules; external-data-bound ones (real residential-proxy
egress, real-device GPUs, large-scale prevalence) are queued with the exact data they'd need — never shipped
on a guess. See grounding.md and calibration.md.
The evader fleet is a ladder of real open-source anti-detect tools, run only against Kitsune's own
detector — scripted TLS-mimicry (curl-impersonate, primp, go-tls/uTLS), Playwright-stealth and
CDP-leak patches (patchright, rebrowser), CDP-native drivers (nodriver, zendriver, pydoll),
isolated-world Selenium (undetected, selenium-driverless), the engine-level frontier (Camoufox),
farbling (Brave), HTTP/2 DoS, and an LLM agent — plus a multi-mode stealth harness that demonstrates
each realm-coherence evasion.
93 of 103 evaders score bot (full matrix, ruleset 0.74.57). The remaining 10 reach only suspicious — the conviction-gate frontier (top evaders, below): they defeat every convicting rule and trip only corroborating tells, which can never reach bot alone.
Each evader is a real anti-detect tool/technique; Caught by is the top convicting tell:
| Evader | Caught by (top convicting tell) | Incoh. | Score | Label |
|---|---|---|---|---|
curl-impersonate |
net.no_js_execution |
0.60 | 0.90 | bot |
nodriver |
br.headless_ua |
0.00 | 0.92 | bot |
full-stealth |
br.cdp_runtime_enabled |
0.60 | 1.00 | bot |
camoufox |
net.tcp_os_vs_ua |
0.84 | 1.00 | bot |
ios-ua-spoof |
br.ch_he_headless |
0.98 | 1.00 | bot |
Top evaders — the conviction-gate frontier (10). These real tools defeat every convicting (coherence/automation/artifact) rule and trip only corroborating tells, so the gate holds them at suspicious — never bot on corroboration alone:
| Evader | Trips (corroborating only) | Score | Label |
|---|---|---|---|
zendriver-uach |
bh.input_entropy_floor, br.hover_none_desktop |
0.50 | suspicious |
webrtc-leak |
net.webrtc_ip_vs_observed, br.media_devices_empty |
0.49 | suspicious |
camoufox-hardened |
br.webrtc_unavailable, bh.power_law_violation |
0.49 | suspicious |
camoufox-linux-coherent |
br.webrtc_unavailable, bh.power_law_violation |
0.49 | suspicious |
camoufox-linux |
br.webrtc_unavailable, bh.power_law_violation |
0.49 | suspicious |
zendriver-uach-behave |
br.hover_none_desktop, br.webrtc_unavailable |
0.49 | suspicious |
camoufox-hardened-behave |
br.webrtc_unavailable, br.media_devices_empty |
0.49 | suspicious |
camoufox-socks-webrtc |
br.webrtc_unavailable, br.media_devices_empty |
0.49 | suspicious |
camoufox-headful |
br.webrtc_unavailable, br.media_devices_empty |
0.47 | suspicious |
patchright-headful |
br.media_devices_empty, br.voices_empty |
0.46 | suspicious |
Per-session detection saturates, so the durable arms race moves up a level: many sessions acting as one adversary. Kitsune runs both sides of it.
Red — Skulk, the fleet adversary-emulation kit (fleet/, authorization-scoped in
scope.py). Skulk emits coordination-shaped sessions to a detector's /ingest — it is a modeling tool,
not a flood/DoS/credential generator. Strategies range from naive to evasive: cloned (one fingerprint
fleet-wide), trace-replay (one canned pointer trace), randomizer/fuzzy (per-node jitter),
fuzzy-rotate / ticket-reuse / ipv6-rotate (within-session rotation), staggered (spread arrivals),
and diffuse-campaign (distinct everything, tuned just above the template floor). The
fleet manager drives real evader containers as named
evasions, plans, archetypes and campaigns — per-node KS_NODE_SEED, per-node proxy, stagger_seconds — and
shards an objective into distinct per-worker behavioral
tasks (synthetic lab targets only — example.test, the harness allow-list; never a real site).
Blue — two coordination scorers over the session corpus:
- Per-binding (
coordination.pyscore_corpus) clusters by JA4 prefix and convicts on cross-instance collisions — fingerprint/trace collision, shared WebRTC origin, TLS ticket reuse — behind a gate that requires ≥2 distinct /64 origins before anything reads as a fleet (a real cohort on one office build must not look like a botnet). - Axis A (
score_campaigns) is the aggregate view: community detection over a multi-dimensional similarity graph (JA4 prefix, behavioral descriptor, lockstep arrival, origin reputation, prevalence tail). It catches the residual correlation a diffuse fleet leaves after it defeats every pairwise binding.
The two compose into an escalation ladder (task adversary-showcase): a cloned fleet falls to the
per-binding scorer; a build-rotating, behavior-jittered fleet evades that yet axis A catches the aggregate;
and only a fully diversified fleet — distinct build/fingerprint/trace, spread descriptors, spread
arrivals — evades both, at which point it is, by construction, N independent real users (the economic bind;
conviction there is external-data-bound). The diversification levers that get a red fleet there map one
to one onto the axis-A dimensions they each defeat. The full loop — strategies, manager, objectives, both
scorers, levers and the showcase — is documented in docs/fleet.md.
The arena (arena/, Go) is a public, self-hosted reproduction of the documented open
web-challenge families — proof-of-work, CAPTCHA (text / math / honeypot), slider, rotate,
emoji / Quick-Draw / procedural-shape image-select, a reCAPTCHA-style checkbox, a Turnstile-style
managed ladder, PACT / Privacy-Pass attestation, a rate-limit and a virtual waiting-room queue —
plus one Kitsune-original gate, track, a real-time visual-tracking challenge — each (where it has a
difficulty axis) at easy / medium / hard. A visitor brings any client to a gate and sees two verdicts at
once: did you solve the challenge, and does your client cohere across layers, read independently over the edge?
That juxtaposition is the whole point: a solved challenge is a cost or Turing test, not a bot/human
discriminator. Every gate here falls to the right scripted solver (evaders/arena-solver,
arena-solver-ocr) — and the detector still convicts the no-JS client on the network layer regardless.
Coherence + attestation is the durable signal; the puzzle is not. The one refinement is track: against
an LLM browser agent (a real, coherent, humanly-paced browser that evades every fingerprint and behavioural
tell), a real-time tracking task is a discriminator — its snapshot→reason(seconds)→act loop clicks a stale
position while a human servos to the live one, convicting it (bh.arena_stale_snapshot), validated live against a
claude-driven agent. Like everything else in the lab, the gates and solvers are vendor-neutral and talk only to
Kitsune's own infrastructure — never a third-party widget.
The field's pages (CreepJS, Sannysoft, pixelscan, …) are single-layer, client-side point-checks. Kitsune's edge is incoherence across layers and time, scored server-side. The three most differentiating mechanisms:
- Within-session temporal incoherence — flags an invariant field that rotates under one session:
TLS (
net.ja4_unstable_within_session), origin (net.ip_rotation_within_session), browser fingerprint (br.fingerprint_unstable_within_session), trajectory replay (bh.trace_replay_within_session). No public fingerprinting page tracks rotation across a session — it catches the re-randomizing anti-detect browser that reuses one cookie. - Coalesced-pointer-event structural tell (
bh.synthetic_no_coalesced/br.coalesced_untrusted) — catches CDP-injected input viagetCoalescedEvents()length +isTrusted, independent of trajectory shape, so a GAN/diffusion mouse-path humanizer that beats every shape metric is still caught. - Worker-realm coherence ladder (
br.worker_source_rewritten,br.worker_constructor_tampered) — convicts worker-scope spoof injection by the blob-URL + constructor-identity round-trip, robust to the entire Proxy-over-native disguise ladder. - Cryptographic agent identity (
net.web_bot_auth_invalid) — verifies an RFC 9421 Web Bot Auth signature at the edge: a valid signer is allow-listed as averifiedagent (sound only under signing-key secrecy), while a forged/replayed signature for a known key convicts. FP-safe by construction — a real browser sends no such headers.
The rest — four-wire-layer ⇄ JS fusion and 2/3-power-law biomechanics
(docs/detection-landscape.md), plus cloud-behind-residential-proxy
(docs/coordination-proxy.md) — round out the gap analysis. Every one was
grounded the same way: confirm the evasion EVADES first (a purpose-built red-team mode), then ship the
detection only once it CONVICTS that evader and stays clean on the calibration FP gate.
Per-session detection saturates; the durable signals are structural, and Kitsune has working models for both the red team flagged:
- Prevalence / likelihood — scores how improbable a fingerprint's joint field combination is under a real-traffic prior. It is the one class that scores a generator-assembled fingerprint with no contradiction. Corroborating-only (its prior is single-source) until a second source validates it.
- Coordination / fleet detection — clusters sessions by JA4 and grades fleets via the TLS-identical-but-JS-divergent paradox + fingerprint-collision + per-launch TLS randomization, plus the aggregate axis-A campaign detector for the residual correlation a diffuse fleet leaves — behind its own conviction gate (a real cohort sharing one browser build must not read as a botnet). The matching red side (Skulk + the fleet manager) and the full red⇄blue ladder are in docs/fleet.md.
| Component | Lang | What it is | Tests |
|---|---|---|---|
contracts/ |
JSON Schema | The stable wire contracts + the rules-as-data registry — the only coupling | CI-validated |
detector/ |
Python | Session correlation, the coherence engine, the conviction-gated scorer, the prevalence model, keyless (DB-IP Lite) City+ASN geo / IP-reputation enrichment | ~100% |
harness/ |
Python | The scoreboard, the calibration precision gate, the coordination scorer, biomech calibration (ethics enforced in code) | ~97% |
edge/ |
Go | TLS→JA3/JA4 (+ GREASE, post-quantum), HTTP/2 (Akamai + JA4H + unknown-engine), TCP/IP-OS, QUIC/HTTP-3 (RFC 9001 decrypt), HTTP/2 DoS attribution | ~97% (fp) |
collector/ |
TypeScript | In-browser fingerprint + behavioral collection + a CreepJS-style live self-test page running the full probe suite | 100% (logic) |
evaders/ |
Py/TS/Go | The red-team ladder of real anti-detect tools (above) | all bot |
fleet/ |
Python | Skulk — the fleet adversary-emulation kit (coordination-shaped sessions; authorization-scoped in code) | ~97% |
arena/ |
Go | Public self-hosted challenge gates (PoW · CAPTCHA · slider · image-select · PACT), each easy/medium/hard | ~95% |
# the Python spine (detector + harness): run the scoreboard demo
cd harness && uv sync && uv run python -m kitsune_harness
# everything, locally (headers · detector · harness · edge · collector)
task ci
# measure the false-positive rate against real browser fingerprints
task calibrateGo and Node aren't required locally — use Docker (golang:1.26-alpine, node:22-alpine) for those.
- Architecture — the design: the pipeline, the coherence engine, the conviction-gated scorer, the structural frontiers, and the calibration discipline.
- Findings — the arms-race narrative: each evasion, the layer that caught it, and why (the Camoufox frontier, the precision turn, the realm-coherence family, the HTTP/2 DoS family, …).
- Calibration · Prevalence model · Coordination & the fleet — the precision gate and the two structural frontiers.
- Fleet & coordination — Skulk, the fleet manager, objectives, both coordination scorers, the diversification levers and the escalation showcase.
- Arena — the public challenge gates (PoW · CAPTCHA · slider · image-select · PACT) and the solved-challenge ≠ human thesis.
- Grounding loop · Research radar — how every rule is grounded, and the external-research intake queue that feeds it.
- Privacy — the public site captures no visitor data: signals are scored in memory and never written to disk, retained, sold, shared, or used to track.
- Detection catalog · Evasion catalog — the blue/red work queues.
- Coverage matrix — every detector rule × every evader.
- Decision records — MADR ADRs for the load-bearing decisions.
- Contributing · Code of Conduct · Security · Changelog
Explore it live (the same data, rendered + cross-linked at kitsune.id): Detections · Evasions · Matrix · Arena · How it works · Research
Evaders target only Kitsune's own detector and a fixed set of public endpoints built for bot/
fingerprint testing (sannysoft, CreepJS, BrowserLeaks, tls.peet.ws, the fingerprint.com demo,
incolumitas). Never a third-party or production site. The allow-list is enforced in code
(harness/.../allowlist.py) — the self-contained arena is
the ethics design.
MIT.