How to use playwright-stealth
Proxy Compare read seven browser signals under playwright-stealth 2.0.3 on Chromium 148, three consecutive runs on 27 Aug 2026, and got byte-identical output each time. It gets the user agent right and then reports two different platforms, which is worse than spoofing nothing.
3 Sept 2026 · 4 min read
Point in time. The figures below were read on 27 Aug 2026 and are not updated after publication. For current numbers see the comparison table.

What is playwright-stealth?
playwright-stealth is a Python package that patches a running Playwright
session to hide the markers that identify it as automated. It is a port of
puppeteer-extra-plugin-stealth, which does the same job in the Node ecosystem
and is where most of the technique originated.
The problem it addresses: a headless browser differs from a real one in ways
that are trivial to read from JavaScript. It announces HeadlessChrome in its
user agent, sets navigator.webdriver to true, ships no plugins, and is
missing the window.chrome object a real Chrome carries. Any page can check all
four in a line of code.
It differs from Patchright in where it intervenes. Patchright patches the framework and you swap an import; playwright-stealth leaves Playwright alone and injects fixes into each page at runtime. That makes it easier to add to an existing script and means the fixes have to be applied to every context you create.
Installing and wrapping a session
pip install playwright-stealth
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth
with Stealth().use_sync(sync_playwright()) as pw:
browser = pw.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
That is the whole setup. The advice usually stops there, so we read back what the page sees afterwards.
It fixes three of the four signals plain Playwright fails. On the fourth it reports two different operating systems in the same call.
What it changes
| Signal | Playwright | playwright-stealth |
|---|---|---|
navigator.webdriver | true | false |
| user agent | HeadlessChrome | Chrome |
window.chrome | undefined | object |
navigator.plugins.length | 0 | 3 |
That is a better result than
Patchright managed by default, which
cleared the webdriver flag and left the other three. It also removes
HeadlessChrome from the user agent, which neither Patchright configuration did.
The contradiction
Three consecutive runs, byte-identical output every time:
navigator.userAgent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ... Chrome/148
navigator.userAgentData.platform macOS
navigator.platform Win32
The user agent says Macintosh. The modern platform API agrees and says macOS. The
legacy navigator.platform says Win32.
A real machine cannot answer that pair two ways. A Mac reports MacIntel; a
Windows machine reports Win32 and does not send a Macintosh user agent. Catching
this needs no fingerprinting library and no heuristic, only an equality check
between two properties any script can read.
We are not claiming this defeats any specific system, and we did not test it against a detector. We are saying the two values disagree, that they disagreed on every run, and that it is trivially readable.
Why the legacy property is the one that matters
navigator.platform is deprecated and every modern guide will tell you to use
navigator.userAgentData instead. That is exactly why it is worth checking: a
stealth patch written against the modern API has no reason to remember the old
one, and a detector written five years ago has no reason to stop reading it.
Deprecated does not mean absent. It means nobody is looking at it except the people looking for you.
Reading the signals yourself
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth
with Stealth().use_sync(sync_playwright()) as pw:
browser = pw.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.evaluate("""() => ({
ua: navigator.userAgent,
platform: navigator.platform,
uaPlatform: navigator.userAgentData?.platform,
webdriver: String(navigator.webdriver),
})"""))
Four lines of JavaScript, and it is worth running against whatever you are using rather than trusting any table, including this one. The projects in this space ship quickly and a result from August is a result from August.
Why the mismatch is reproducible
Camoufox changes the operating system it claims between launches, and every run
agrees with itself: a Mac user agent arrives with MacIntel, a Windows one with
Win32. We measured that in
how to choose an anti-detect browser.
This is the opposite shape. The values do not vary at all, so the mismatch is reproducible rather than intermittent, which makes it something you can check for in one line before a run rather than a ghost you chase through a log.
Adding a proxy
playwright-stealth wraps Playwright, so the proxy argument is unchanged:
with Stealth().use_sync(sync_playwright()) as pw:
browser = pw.chromium.launch(
headless=True,
proxy={"server": "http://gate.example.com:7000",
"username": "user-session-abc123",
"password": "secret"},
)
The two things that break this are provider-side rather than library-side. The
session token sits in the username at some providers and the password at
others, and the wrong one produces a 407 with nothing naming the cause. And
some providers take no username and password at all, wanting your IP on an
allowlist instead.
We publish what each provider states about both on its provider page with the date we read it, and print that a provider says nothing where it says nothing. The format converter reshapes a credential into the separate fields this call expects.
Limitations
No anti-bot system, no evasion claim, no recommendation. Seven signals from playwright-stealth 2.0.3 on Chromium 148, one machine, three runs, one day. The value here is a check you can re-run in a minute, not a verdict with a shelf life.
One boundary worth naming: this guide exists for pages you do not control. If the captcha you keep meeting is on your own application, stealth is the wrong tool entirely, and testing captcha flows in Playwright with the vendors' test keys is the shorter and more honest path.
Sources
- playwright-stealth PyPI, read 27 Aug 2026
playwright-stealth 2.0.3
Questions
Does playwright-stealth work?
On three of the four signals we checked, yes. It cleared navigator.webdriver, restored window.chrome to an object and gave navigator.plugins a non-zero length, all of which plain Playwright fails. On the fourth it left navigator.platform reading Win32 while the user agent and navigator.userAgentData both said macOS.
Why does navigator.platform say Win32?
We do not know why, and we are not guessing. What we can say is that it was Win32 on all three runs on 27 August 2026 at version 2.0.3, while userAgentData.platform said macOS in the same call, so the legacy API and the modern one disagreed with each other.
Is playwright-stealth better than Patchright?
On the signals we read it fixed more of them. Patchright cleared only navigator.webdriver by default and left window.chrome undefined and plugins empty. playwright-stealth fixed all three and introduced the platform disagreement, so neither was complete.
