liarjs.dev
← Field notes
Benchmarks

I scored eleven Chromium stealth setups with my own tool. One launch flag ties the libraries, and the most popular plugin loses.

September 12, 2026 · 5 min read

Every table comparing anti-detect tooling is published by someone selling one of the entries. This one is published by someone selling nothing - liarjs is free and the checks are an MIT package - but I am obviously not neutral either, so the useful thing is not my opinion. It is that you can regenerate every number below with one command.

liarjs scores for eleven Chromium automation stacks

How it was measured

Each stack launches a browser with a debug port, and then the same command scores it:

npx liarjs --cdp http://127.0.0.1:<port> --page https://example.com/ --json out.json

Three details that decide whether the comparison means anything:

The page has to be an https page. On the default about:blank the tool itself warns that UA-CH, StorageManager and most Permissions are unavailable, and the API-surface check degrades to “12 of 53 interfaces present, re-run on https to evaluate”. Scoring every stack on a degraded check does not produce a broken comparison, it produces a quietly wrong one.

Two deductions belong to my laptop, not to any tool. A control run with no stealth at all reports IP timezone vs browser timezone -12 (the exit resolves to Asia/Hong_Kong, the browser says Asia/Shanghai) and speech voice vs locale -8 (voice en-US, locale zh-CN). Those hit every stack identically and measure my network and my system language. Both columns are below: the raw score, and the score with that host noise added back.

The baseline is not 100. Unpatched headless Chromium scores 0. Headed scores 48. What is worth reading is the movement from that baseline, not the absolute number.

The table

+host is the host noise described above.

Stack raw +host adjusted navigator.webdriver critical findings
patchright, Chrome channel, headed 80 20 100 clean none
playwright, Chrome, headed + one flag 80 20 100 clean none
playwright, Chromium, headed + one flag 88 12 100 clean none
rebrowser, Chrome, headed + one flag 80 20 100 clean none
playwright, Chrome, headed 40 20 60 set webdriver
playwright, Chromium, headed 48 12 60 set webdriver
rebrowser, Chrome, headed 40 20 60 set webdriver
puppeteer, headed 42 12 54 set webdriver
puppeteer-extra-plugin-stealth, headed 18 12 30 clean gpu-triad, worker-consistency, gl-ext-formfactor, webgpu-features
puppeteer-extra-plugin-stealth, headless 18 12 30 clean same four
puppeteer, headless 12 12 24 set webdriver, headless-ua
playwright, Chromium, headless 0 20 20 set webdriver, headless-ua, perm-notif, chrome-object

The flag is --disable-blink-features=AutomationControlled.

The stealth plugin scores below the thing it patches

This is the result worth the post. Diffing puppeteer-extra-plugin-stealth against plain puppeteer, check by check, it fixes exactly one thing and breaks four:

  • fixed: navigator.webdriver goes from true to false
  • broken, -22: WebGL reports an Intel GPU while navigator.gpu’s adapter.info.vendor says apple. The two interfaces name different hardware.
  • broken, -20: a Web Worker reports different languages, hardwareConcurrency and webglRenderer than the main thread. The override is installed on the main thread only. Detectors re-read identity inside workers for exactly this reason.
  • broken, -16: the renderer string names a desktop Intel GPU, and the extension list next to it carries mobile-only compressed texture formats - WEBGL_compressed_texture_pvrtc, astc, etc.
  • broken, -12: navigator.gpu advertises compression features that come from the adapter Chrome actually opened, which is not the adapter the page claims to be on.

Hiding one tell and manufacturing four louder ones is the whole argument for scoring consistency rather than counting properties. A property-counting checker sees a clean navigator.webdriver and stops. A consistency checker asks whether the WebGL story, the WebGPU story and the worker’s story are the same story, and here they are three different ones.

One honest limit: this machine is Apple Silicon, so part of the gpu-triad mismatch - real apple against a spoofed intel - is a property of the host. On an Intel machine that specific check may not fire. The other three do not depend on it.

One launch flag ties the library

patchright on the Chrome channel scores 80. Plain playwright on the Chrome channel with --disable-blink-features=AutomationControlled also scores 80, and comparing the two results check by check, not one check differs in status.

That is not “patchright is pointless”. Patchright’s patches target the CDP world and Runtime.Enable, and this passive layer does not score those separately. It is a narrower statement, and it is the one the data supports: against this particular instrument, installing a replacement library and adding one launch argument buy the same thing.

The same care applies to rebrowser-playwright-core. Its patches fix Runtime.Enable and the utility world name. They never claimed to touch navigator.webdriver, so its bare 40 is not a failure - it is a tool not doing something it does not advertise. Add the same flag and it is 80 like the rest.

What this does not show

  • Passive layer only. The behavioural challenge runs only on this site and needs a real pointer; none of it is in this table.
  • One run per stack. No repeats, no confidence intervals.
  • patchright’s bundled Chromium is missing. patchright install chromium stalled at 0% of 171 MiB from this network, so the patchright row uses the Chrome channel.
  • No camoufox. It is Firefox-based and does not speak CDP, so this harness cannot attach to it.
  • These are one version of liarjs on one day. Change the weights and the numbers change.

If you want to run it against your own stack, the command is at the top and the checks are an MIT package: npx liarjs.

Sources & further reading
  1. liarjs CLI (the instrument used here)
  2. puppeteer-extra-plugin-stealth
  3. patchright
  4. rebrowser-patches
More field notes
← Back to field notes