"Playwright stealth" usually means one of two open-source add-ons: playwright-extra with puppeteer-extra-plugin-stealth for Node.js, or the playwright-stealth package for Python. Both change values that an automated Chromium exposes, such as navigator.webdriver, so the browser looks more like one a person started. Their own authors say this has limits.
This guide is for people who automate their own workflows, test their own sites or collect data where they are allowed to. It explains what the plugins do, which evasion modules they ship, how current they are, what their authors say about them, and an alternative: driving a real, persistent browser profile with Playwright's connectOverCDP. All versions and quotes were checked on 24 October 2026. It is not a guide to getting past any particular website's bot protection; follow each site's terms.
What is Playwright stealth?
Playwright stealth is a set of patches applied to a Playwright-controlled browser that change the values most often used to spot automation. It doesn't change how Playwright works; it changes what a page can read.
Automated browsers differ from a normal Chrome window in small ways. The navigator.webdriver flag is true under automation, headless mode can change other values such as the user agent, and objects a normal Chrome window has, such as parts of window.chrome, may look different. Stealth plugins override these values when each page loads, and adjust some launch arguments.
What are playwright-extra and the stealth plugin?
playwright-extra is a wrapper around Playwright that lets you add plugins, and puppeteer-extra-plugin-stealth is the best-known plugin for it. Both live in the berstend/puppeteer-extra monorepo and are MIT-licensed.
The playwright-extra README says it "is compatible with most puppeteer-extra plugins" and shows the stealth plugin added with chromium.use(StealthPlugin()).
| Package | Latest version | Published | License |
|---|---|---|---|
playwright-extra |
4.3.6 | 1 March 2023 | MIT |
puppeteer-extra-plugin-stealth |
2.11.2 | 1 March 2023 | MIT |
puppeteer-extra |
3.3.6 | 1 March 2023 | MIT |
None of the packages is marked deprecated on npm, and we found no maintenance statement in the READMEs. We couldn't verify the repository's current activity. What we can say is that the latest releases are from March 2023, while Chrome and Playwright have shipped many versions since.
Which evasions does the stealth plugin include?
Version 2.11.2 enables 16 evasion modules by default (from index.js in the published package):
chrome.app,chrome.csi,chrome.loadTimes,chrome.runtime: add thewindow.chromeobjects a normal Chrome window has.defaultArgs: changes default launch arguments.iframe.contentWindow: adjusts how iframes expose their window.media.codecs: reports the media codecs a normal Chrome supports.navigator.hardwareConcurrency,navigator.languages,navigator.permissions,navigator.plugins,navigator.webdriver: adjust thosenavigatorvalues.sourceurl: deals with the source URL marker that injected scripts can leave behind.user-agent-override: overrides the user agent and related values.webgl.vendor: changes the WebGL vendor and renderer strings.window.outerdimensions: sets the outer window size.
A navigator.vendor module exists in the package but isn't in the default set.
What is the Python playwright-stealth package?
The Python playwright-stealth package brings similar evasions to Playwright for Python. Its latest release is 2.0.3, released 4 April 2026, after 2.0.0 in June 2025 and 1.0.6 in September 2023. It is MIT-licensed and maintained by Mattwmaster58, whose README describes it as a "Fork of AtuboDad's port" of the Node.js plugin.
Its options in stealth.py are on by default, except chrome_runtime, which is off:
| Group | Options |
|---|---|
window.chrome |
chrome_app, chrome_csi, chrome_load_times, chrome_runtime (off by default) |
navigator |
navigator_hardware_concurrency, navigator_languages, navigator_permissions, navigator_platform, navigator_plugins, navigator_user_agent, navigator_user_agent_data, navigator_vendor, navigator_webdriver |
| Headers | sec_ch_ua |
| Graphics and media | webgl_vendor, media_codecs, hairline |
| Other | iframe_content_window, error_prototype |
Compared with the Node.js plugin, it adds options for navigator.platform, User-Agent Client Hints (navigator_user_agent_data and sec_ch_ua), navigator.vendor and error prototypes.
What do the authors say about the limits?
Both projects' authors say plainly that stealth patches can't hide automation completely. Take that as the most reliable statement about them.
The stealth plugin's README says: "It's probably impossible to prevent all ways to detect headless chromium". It calls the work "a friendly competition in a rather interesting cat and mouse game". The same "Status" section claims the plugin "passes all public bot tests", and says reCAPTCHA v3 scores should be taken "with a grain of salt". Public test pages change, so a claim like that describes one moment.
The Python package is blunter. Its README says: "Don't expect this to bypass anything but the simplest of bot detection methods." It also calls itself "a proof-of-concept starting point."
Test pages look for exactly these patches. The CreepJS research page shows "headless" and "stealth" percentages, and its README lists puppeteer-extra among the tools its tests target. A patched value that doesn't fit the rest of the browser can stand out more than the original value. Our guide to browser fingerprinting explains why consistency matters more than any single value.
Why patches have limits
Stealth plugins work from the outside: they rewrite values in each page after the browser has started. That leaves three gaps.
- Not every signal is a JavaScript value. Headers, TLS details and the network path aren't changed by overriding
navigatorproperties. - Patches can be seen. A function that has been replaced can behave slightly differently from the original, which is what tests looking for "lies" check.
- The rest of the profile still matters. A fresh, empty context with no history, no cookies and a data center IP looks like a new visitor every time, however well its
navigatorvalues are patched.
That last point is often the real issue for legitimate automation. If you automate your own accounts, you usually want a persistent browser: the same cookies, the same settings and the same IP every time, as if you had opened it yourself.
How do you connect Playwright to a real browser profile?
Instead of launching a fresh, patched browser, you can start a persistent Chromium-based profile and attach Playwright to it with browserType.connectOverCDP. The profile keeps its own cookies, storage, extensions and proxy between runs.
Playwright documents connectOverCDP as attaching "Playwright to an existing browser instance using the Chrome DevTools Protocol". It has been available since v1.9. Note the caveats in the same docs:
- It is "only supported for Chromium-based browsers".
- The connection has "significantly lower fidelity" than the Playwright protocol used by
browserType.connect(). - If the browser wasn't launched with Playwright's own arguments, "some of Playwright functionality may be broken".
- The endpoint can be an HTTP address such as
http://localhost:9222/or aws://…/devtools/browser/…URL. The browser's existing context is available throughbrowser.contexts(). - Newer options include
noDefaults(v1.60), which leaves the existing default context's settings alone, andisLocal(v1.58).
So test your script against the features you need, and expect some Playwright features to behave differently than with a browser Playwright launched itself.
Example: Playwright with a Multime profile
Multime runs each profile as its own Chromium-based browser with its own cookies, storage, extensions, proxy and fingerprint settings. Its local API runs on your own computer (port 50325 by default, API key required by default) and opens a profile and returns the DevTools endpoint for Puppeteer, Playwright or Selenium. The local API is part of the Solo plan and higher.
This Node.js script opens profile number 12, attaches Playwright, does its work and closes the profile through the API:
import { chromium } from 'playwright'
const API = 'http://127.0.0.1:50325'
const headers = { Authorization: `Bearer ${process.env.MULTIME_API_KEY}` }
// Open the profile (serial_number is the No. column in Multime)
const res = await fetch(`${API}/api/v1/browser/start?serial_number=12`, { headers }).then((r) => r.json())
if (res.code !== 0) throw new Error(res.msg)
// Attach Playwright to the running profile
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${res.data.debug_port}`)
const context = browser.contexts()[0]
const page = context.pages()[0] ?? (await context.newPage())
await page.goto('https://example.com')
console.log(await page.title())
// Close the profile through Multime so its cookies and tabs are saved
await fetch(`${API}/api/v1/browser/stop?serial_number=12`, { headers })
Some notes from the API reference:
- Every response has the shape
{code, msg, data}; checkcodebefore readingdata. - Close the profile with
/api/v1/browser/stoprather thanbrowser.close(), so Multime saves the session and releases the profile for your team. /api/v1/browser/activetells you whether a profile is already open.- The API follows AdsPower's format for its core endpoints, but not every AdsPower endpoint is implemented. Run any script on a test profile first.
A real profile doesn't make automation invisible, and no tool can promise that. What it gives you is a stable, separate browser per account, so your script works in the same environment you would use by hand. For runs across many profiles without code, the automation editor runs flows on up to 20 profiles in parallel.
When to use which approach
Use stealth plugins for short-lived, stateless jobs on sites you are allowed to automate, knowing their authors' own warnings. Use a persistent profile when the job belongs to an account you run and should look the same every time.
| Need | Stealth plugin | Persistent profile with connectOverCDP |
|---|---|---|
| Fresh browser per run | Yes | Possible, but not the point |
| Cookies and logins kept between runs | You manage storage yourself | Kept by the profile |
| Its own proxy per account | Set per launch | Set per profile |
| Full Playwright fidelity | Yes, Playwright launches the browser | Lower, as Playwright's docs warn |
Whichever you choose, respect each site's terms of service and robots.txt, keep request rates reasonable and don't automate actions a platform's rules forbid.