The library for faithful browser automation.
Why the current stack fails. patchright, puppeteer-real-browser, nodriver, and undetected-chromedriver all randomize fingerprint surfaces independently — pick a UA, pick a hardwareConcurrency, pick a WebGL renderer, hope nothing cross-references. A single probe that compares two surfaces breaks the spoof. They also run HTTP fetches out-of-band through the runtime's stock TLS stack, so JA4 reveals the spoofed Chrome is not a Chrome. They synthesize at most a mouse helper, not a biomechanical model. And they're patches against a moving Chromium target, not a coherent design.
What mochi does differently. Every fingerprint surface derives from one (profile, seed) pair through a 48-rule deterministic DAG — a Mac UA never lands next to Linux WebGL. All network traffic — page.goto, in-page fetch, and session.fetch — routes through Chromium itself, so JA4/JA3/H2 are real Chrome by construction. There is no parallel HTTP layer. humanClick/humanType/humanScroll are full Bezier+Fitts+lognormal-digraph models, parameterized off the matrix's behavior block. One library owns the whole pipeline.
| mochi | patchright | puppeteer-real-browser | nodriver | undetected-chromedriver | |
|---|---|---|---|---|---|
Relational (profile, seed) matrix |
yes | no | no | no | no |
Chromium-native session.fetch |
yes (browser-routed) | no | no | no | no |
| Behavioral synthesis (Bezier+Fitts+jitter) | yes | no | mouse-helper only | mouse-only | no |
| Single-runtime stack | yes (Bun) | yes (Node) | yes (Node) | yes (Python) | yes (Python) |
| Probe-Manifest harness as CI gate | yes | no | no | no | no |
How to get started.
bun add @mochi.js/core @mochi.js/cli
bunx mochi browsers install// hello-mochi.ts
import { mochi } from "@mochi.js/core";
const session = await mochi.launch({ profile: "linux-chrome-stable", seed: "user-12345" });
try {
const page = await session.newPage();
await page.goto("https://httpbin.org/headers");
console.log(session.profile.userAgent);
} finally {
await session.close();
}Full walkthrough: mochijs.com/docs/getting-started/quickstart. One-page comparison deep-dive: mochijs.com/docs/reference/comparison.
mochi v0.4.0 on a Linux datacenter IP (Aixit GmbH, hosting ASN, Frankfurt) scored bot: not_detected, suspect_score: 8 against FingerprintJS Pro v4. Patched Chrome reports 12 in comparable conditions; CloakBrowser 18. The tampering ML detected something — tampering_ml_score: 0.9853 — but the bot classifier did not promote because the relational fingerprint was internally coherent across every axis.
Everyone told you to spoof Windows. They were wrong. Linux has 4% desktop market share but is massively overrepresented in high-LTV segments — developers, engineers, researchers. WAFs trained on real traffic don't flag Linux because Linux is real users. The signal was always
HeadlessChrome, not Linux. mochi defaults to host-OS matching: a Linux server runs the linux profile.
Full evidence and architectural rationale: reference/comparison · concepts/stealth-philosophy.
People use mochi to scrape, to QA-test against staging WAFs, to debug WAF rules they think over-block, to build data pipelines, to run cross-browser regression suites, to simulate users for performance work, to research how detection systems actually behave. The mechanics are identical in every case — you want a real Chrome session that doesn't get caught in measures designed for actual attackers. mochi gives you that by being consistent, not by being deceptive.
We don't sort our users by intent. If your threat model is "don't get traced," mochi is the wrong tool — it's open source and the fingerprint profiles ship in the package, which means a sophisticated attacker treats them as a known signature to avoid. mochi is sized for developers who want their automation to look like a real Chrome.
- Relational locking, not randomization. Every fingerprint surface (canvas, WebGL, audio, fonts, timing, MediaDevices, WebGPU, …) derives from a single
(profile, seed)pair through a 48-rule DAG. No Frankenstein fingerprints — a Mac UA never lands next to Linux WebGL. - Zero-jitter spoofing. A single ~50KB inject payload runs at top-of-frame. JIT-friendly Proxy traps, no async round-trips when a WAF micro-times
performance.now(). - Inject delivery without the source-attribution leak.
Fetch.fulfillRequestbody splice on Document responses (CSP rewriter included), withPage.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" })as theabout:blankfallback. Source-byte-indistinguishable from a same-origin developer's own<script>tag. - Behavioral synthesis.
humanClick/humanType/humanScrollderive from biomechanical models — Bezier paths with overshoot+correction, Fitts-law movement times, lognormal digraph delays, Gaussian jitter — all parameterized per profile (hand,tremor,wpm,scrollStyle). - No parallel HTTP layer.
session.fetch(url)routes through Chromium itself via CDP —Network.loadNetworkResourcefor simple GETs,page.evaluate("fetch")for everything else. JA4 / JA3 / H2 are real Chrome by construction because Chromium is the client. Cookies inherit from the page's origin; proxy egress is shared withpage.goto. - Probe-Manifest harness.
bun run harness:smokecaptures a Probe Manifest from the live session and diffs it against per-profile baselines. Zero-Diff is a CI gate; intentional divergences live inexpected-divergences.jsonnext to a rationale. - Stock Chromium. No forks, no patches, no proprietary infrastructure. Pinned Chromium-for-Testing, auto-downloaded by
mochi browsers install. BYO viabinary: <path>.
Direct port from the Limits page — the architectural-honesty document. Every entry there has a root cause and a tracking link. mochi gives you the best possible JS-layer answer for stealth automation against Chromium-family WAFs; some things genuinely require a Chromium patch and we name them.
| Surface | v0.1 status | Notes |
|---|---|---|
CDP pipe transport (--remote-debugging-pipe) |
works | No TCP port, no Runtime.enable. |
Page.goto / content / evaluate |
works | evaluate is Runtime.callFunctionOn-based — JSON-serializable returns only. |
Page.goto({ waitUntil: "networkidle" }) |
partial | Mapped to "load" until per-frame Network.enable lands. |
| Relational fingerprint Matrix (48 rules) | works | (profile, seed) → MatrixV1, deterministic, JSON round-trippable. |
| JS-layer spoofing (UA / UA-CH, navigator, WebGL, WebGPU, MediaDevices, Permissions, screen, fonts, timezone, locale) | works | Inject payload, JIT-proxy traps, top-of-frame. |
Audio (OfflineAudioContext) byte-accurate fingerprint |
works | Per-(profile, sample-rate) captures consumed via R-047 → audio-fingerprint inject module. The spoof distributes the residual across the 489 samples in [4510..4999) (using Math.fround to model f32 readback) so the page-side digest equals the captured baseline byte-exactly on every host architecture, not just Mac M-series. |
Canvas (toDataURL) byte-accurate fingerprint |
works | Per-profile data URL synthesis via R-048 → canvas-fingerprint inject module. Intercepts probe-sized canvases (300×150) with the captured baseline; probe-side hashString(url) + length + first-50-char prefix match byte-exactly. Non-probe sizes fall through to native rendering. |
Behavioral synthesis (humanClick / humanType / humanScroll) |
works | Bezier+Fitts+jitter; profile-parameterized (hand, tremor, wpm, scrollStyle). |
Profile catalog (mac-m4-chrome-stable, mac-chrome-stable, mac-chrome-beta, windows-chrome-stable, linux-chrome-stable, mac-brave-stable) |
works | Six real-device baselines captured against real Chrome on real devices, each filtered by FingerprintJS Pro suspectScore <= 20 and validated by the harness round-trip. Other catalog ids (mac-m2-…, mac-intel-…, win11-edge-…) still resolve to the generic placeholder. |
Trace recording / replay (mochi record → humanClick(sel, { trace })) |
deferred | API surface forward-compatible; recorder lands in v1.x. |
Browser-routed session.fetch (JA4/JA3/H2-coherent) |
works | Routes through CDP — Network.loadNetworkResource for GETs, page.evaluate("fetch") for non-GET. Real Chrome JA4 by construction; cookies inherit from the page's origin; CORS applies for non-GET cross-origin calls. |
session.fetch on FreeBSD / Alpine musl / Windows arm64 |
works | Routes through the running Chromium process, so any host that runs CfT runs session.fetch. |
Page.screenshot |
works | PNG/JPEG/WebP via CDP Page.captureScreenshot; fullPage, clip, omitBackground, quality, encoding opts. Element-bounded capture ({ element: handle }) is a separate brief. |
| Proxy auth (HTTP/HTTPS/SOCKS5) | works | Inline URL or ProxyConfig shape; CDP Fetch.authRequired, no extension. |
Cookie persistence (Session.cookies.{save,load}) |
works | JSON file with version header + regex domain filter. Round-trips losslessly. |
Page.localStorage.{get,set} / Page.sessionStorage |
works | DOMStorage CDP, frame-scoped (defaults to current main-frame origin). |
Page.grantAllPermissions() |
works | Wraps Browser.grantPermissions with the full descriptor list. Pairs with R-036. |
| Proxy-PAC scripts | not yet | Use system network policy until the flag lands. |
| Turnstile auto-click | works | @mochi.js/challenges — opt-in via challenges: { turnstile: { autoClick: true } }. Visible-checkbox variants only; image / audio / managed escalations fire onEscalation instead of clicking blindly. |
Init-script delivery without Page.createIsolatedWorld |
works | Dual-mechanism: Fetch.fulfillRequest body-splice on Document responses (CSP-rewritten) plus Page.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" }) fallback for about:blank and other non-HTTP nav targets. Idempotency guard via __mochi_inject_marker. |
bot.incolumitas.com anti-debugger trap |
known limit | C++-only fix path. Every CDP-driven tool trips it identically. |
deviceandbrowserinfo.com/are_you_a_bot worker-injection trap |
known limit | Same anti-debugger family as incolumitas. |
fingerprint.com/web-scraping (datacenter IP, cold session) |
known limit | Server-side IP-class scoring; route through residential. |
| Cross-engine FPU / JIT divergence (Safari-from-Chromium) | out of v1 scope | v1 is Chromium-family only. |
| Mobile / touch profiles | out of v1 scope | v2 roadmap. |
The full limits document has the per-vector root-cause analysis. Read it before opening an issue saying "X site detects mochi" — half the answers are already there.
mochi's peer group is the JS-layer stealth-automation tools that drive stock or near-stock Chromium. Each row below is a structural axis, not a marketing axis. The Comparison page has the deep version: each claim cites a specific upstream-source line range you can follow.
| mochi | patchright | puppeteer-real-browser (archived) | nodriver | undetected-chromedriver | |
|---|---|---|---|---|---|
| Runtime | Bun ≥ 1.1 | Node | Node | Python | Python |
| Browser | stock CfT | stock Chromium | stock Chrome + helpers | stock Chrome | stock Chrome (patched binary) |
Runtime.enable avoided |
yes (asserted) | yes | no | partial | n/a (WebDriver) |
Page.createIsolatedWorld avoided |
yes | yes | no | yes | n/a |
Relational (profile, seed) Matrix |
yes | no | no | no | no |
| JS-layer fingerprint coverage (48-rule DAG) | yes | partial (~12 patches) | partial (fingerprint-injector add-on) | partial | partial (flag-level) |
| Probe-Manifest harness as CI gate | yes | no | no | no | no |
Behavioral synthesis (humanClick/humanType) |
yes (Bezier+Fitts+jitter) | no | mouse-helper only | mouse-only | no |
JA4/JA3/H2-coherent session.fetch |
yes (browser-routed) | no | no | no | no |
Single-runtime stack (no pip install next to npm install) |
yes | yes | yes | yes (Python only) | yes (Python only) |
| Turnstile auto-click | yes (@mochi.js/challenges) |
yes | yes | partial | partial |
| Stable-Chrome quirks accumulated over 4+ years | no | partial | partial | yes | yes |
| Ecosystem maturity (issues / PRs / community) | new | mid | mid | mid | high |
Where mochi wins today: relational consistency, no parallel HTTP layer, behavioral synthesis depth, harness-as-gate, single-runtime stack.
Where mochi loses today: ecosystem age, Turnstile auto-click polish, accumulated quirks-fixes from years of production deployment.
Deep version, with per-library audit reports: mochijs.com/docs/reference/comparison.
┌──────────────────────── User code (TypeScript) ────────────────────────┐
│ import { mochi } from "@mochi.js/core"; │
└────────────────────────────────┬───────────────────────────────────────┘
│
┌────────────────────▼────────────────────┐
│ @mochi.js/core — launch, CDP pipe, │
│ Page, Session, │
│ Session.fetch (CDP) │
└────┬─────────────────┬──────────────────┘
│ │
┌────────▼──┐ ┌───────────▼────────┐
│ inject │ │ behavioral │
│ (payload) │ │ Bezier+Fitts │
└────┬──────┘ └────────────────────┘
│
┌────▼──────────┐
│ consistency │
│ (Matrix DAG) │
└────┬──────────┘
│
┌────▼──────────┐
│ profiles │ ◄──── @mochi.js/harness
│ (data) │ (Probe Manifest diff,
└───────────────┘ PR gate / nightly)
- Site: mochijs.com — landing, quickstart, reference, and the live
docs/content collection. - Quickstart — 5-minute walkthrough, copy-pasteable.
- Is mochi for me? — read this if you're choosing between mochi and a peer.
- The Consistency Engine, Stealth philosophy — concept pages.
- Limits — every known limit, with root cause and workaround.
PLAN.md— design contract. The 8 architectural invariants live in §2.CONTRIBUTING.md— contributor workflow (clone → branch → PR, conventional commits, CI gate).CHANGELOG.md— release notes.packages/challenges/README.md— Turnstile auto-click and thechallengesconvenience layer.
For common bot-defense widgets, mochi ships opt-in convenience layers under @mochi.js/challenges. These are thin wrappers around the existing inject + behavioral pipelines — no new fingerprint surface.
const session = await mochi.launch({
profile: "linux-chrome-stable",
seed: "user-12345",
challenges: { turnstile: { autoClick: true } },
});
// Every page from this session auto-clicks visible Turnstile checkboxes.v0.2 covers: Cloudflare Turnstile (visible-checkbox variants only). Image/audio/managed escalations fire an onEscalation callback rather than clicking blindly. See packages/challenges/README.md and the Limits page.
mochi is invariant I-3 in PLAN.md: Bun ≥ 1.1 only, no Node, no Deno. The reasons are concrete and load-bearing:
- Pipe-mode CDP —
Bun.spawnexposes file descriptors 3 + 4 directly to user code, which is what--remote-debugging-pipeneeds. Node'schild_processdoesn't, so every Node-based stealth tool falls back to TCP — and a listening CDP port is a fingerprintable surface. - Bun:SQL — the offline-first profile lookup and
bun workorchestrator both useBun.SQL(libSQL-backed). Nobetter-sqlite3native dep, no migration story, no platform-specific build issues. Bun.spawn+Bun.serveergonomics — the harness fixture server,mochi captureProbe Manifest collector, and the conformance-suite proxy chain all rely on Bun-native primitives that have no zero-cost equivalent in Node land.
If you need a Node-runtime stealth tool today, patchright and puppeteer-real-browser are the live options.
Foundations in main; first npm release 2026-05-08. @mochi.js/core 0.8.0 drops the Rust wreq cdylib entirely — Session.fetch now routes through Chromium itself via CDP (real Chrome JA4 by construction; cookies inherit from the page's origin; proxy egress shared with page.goto). No parallel HTTP layer remains. 0.8 also retunes ALL_BROWSER_PERMISSIONS for Chromium 148. Earlier 0.5/0.6/0.7 lines shipped Page.screenshot, the cookies / localStorage / sessionStorage / grantAllPermissions DX cluster, the Fetch.fulfillRequest dual-mechanism inject, byte-exact audio + canvas fingerprint blobs, and the host-OS-matching profile auto-pick (mochi.defaultProfileForHost()). Public API is stable; new surfaces are additive. The harness Zero-Diff gate runs on every PR. See CHANGELOG.md for what shipped where.
If you found this from somewhere and you're wondering whether to depend on it for production traffic: not yet. The "what works / what doesn't" matrix above is the honest cut. v1.0 will say so plainly.
CONTRIBUTING.md covers the workflow: clone → branch → PR, conventional commits with package scopes, the gates CI runs (typecheck, lint, test, contract, harness Zero-Diff), and the documentation-discipline rules (every limit landed in reference/limits.md in the same PR as the gap it documents).
Stands on the shoulders of:
- nodriver — the no-
Runtime.enablephilosophy. - rebrowser-patches — leak vector documentation.
- patchright — prior art on CDP-level stealth.
- CloakBrowser — stealth conformance test bar.
MIT.
