Playwright stealth: setup and limits.

playwright-stealth hides automation tells such as navigator.webdriver. How to set it up in Python or Node, what its docs say it won’t do, and what to do about hCaptcha.

What playwright-stealth is

Playwright drives a real browser, but an automated one reports itself in ways a page script can read: navigator.webdriver is true, headless Chrome puts HeadlessChrome in its user agent, and several browser objects look different from an ordinary Chrome. A stealth package runs patches in every page before the site’s own scripts load, so those values read like a normal browser’s. There are two packages, one per language:

The two playwright-stealth packages (versions read from PyPI and npm on 2 November 2026)
PackageForLatest versionWhat it is
playwright-stealth (PyPI)Python2.0.3, released 4 April 2026A fork of AtuboDad’s Python port of the Node stealth plugin, maintained by Mattwmaster58. MIT licence, Python 3.9+
playwright-extra + puppeteer-extra-plugin-stealth (npm)Node4.3.6 and 2.11.2, both published 1 March 2023A plugin wrapper around Playwright, plus the stealth plugin from the puppeteer-extra project. MIT licence

Both trace back to the same project. puppeteer-extra-plugin-stealth was written for Puppeteer, and playwright-extra lets Playwright load it. The Python package on PyPI started as a port of that plugin, and the Mattwmaster58 fork is what pip install playwright-stealth installs today.

What it patches

Each patch is called an evasion, and each can be turned on or off. The Python package’s options and the plugin’s evasions folder list the same core set:

  • Automation flag: navigator.webdriver.
  • Identity strings: the user agent, navigator.platform, navigator.vendor, navigator.languages, navigator.userAgentData and the Sec-CH-UA header.
  • Browser objects headless Chrome lacks or fakes badly: window.chrome (app, csi, loadTimes, runtime), navigator.plugins, navigator.permissions and media codecs.
  • Hardware and rendering: the WebGL vendor and renderer strings and navigator.hardwareConcurrency.
  • Frames: iframe.contentWindow, patched so frames built with srcdoc do not give automation away.

We ran both quick starts below on 2 November 2026 with Playwright 1.64.1 and headless Chromium. Without stealth, navigator.webdriver read true and the user agent said HeadlessChrome; with either package it read false and the user agent said Chrome.

Install and use it in Python

Shell · pip
pip install playwright-stealth
playwright install chromium

The recommended way in version 2 is to wrap Playwright’s context manager, so every browser and page you open gets the patches:

Python · playwright-stealth 2.x
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth

# Every browser, context and page opened inside this block gets the patches.
with Stealth().use_sync(sync_playwright()) as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://your-site.example/")
    print(page.evaluate("navigator.webdriver"))   # False (True without stealth)
    browser.close()

The async API is the same with use_async and async_playwright(). To patch a single context or page instead, or to change the defaults, build a Stealth object with keyword arguments:

Python · per-context and options
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth, ALL_EVASIONS_DISABLED_KWARGS

stealth = Stealth(navigator_languages_override=("fr-FR", "fr"))

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    stealth.apply_stealth_sync(context)     # only this context is patched
    page = context.new_page()

# Only the navigator.webdriver patch, nothing else:
webdriver_only = Stealth(**{**ALL_EVASIONS_DISABLED_KWARGS, "navigator_webdriver": True})

Older tutorials call stealth_sync(page). That function went away in 2.0, and on 2.0.3 importing it raises an ImportError; apply_stealth_sync is its replacement.

Install and use it in Node

Shell · npm
npm install playwright playwright-extra puppeteer-extra-plugin-stealth
npx playwright install chromium

playwright-extra is a drop-in replacement for the playwright import; you add the plugin once and use Playwright as normal. The example uses top-level await, so save it as an .mjs file:

Node · playwright-extra
import { chromium } from 'playwright-extra'
import StealthPlugin from 'puppeteer-extra-plugin-stealth'

chromium.use(StealthPlugin())

const browser = await chromium.launch({ headless: true })
const page = await browser.newPage()
await page.goto('https://your-site.example/')
console.log(await page.evaluate(() => navigator.webdriver))   // false
await browser.close()

StealthPlugin takes an enabledEvasions set to choose which patches run, and single evasions such as stealth/evasions/webgl.vendor take their own options through chromium.plugins.setDependencyDefaults; the playwright-extra README shows how.

What its own docs say it does not do

Both projects are frank about their limits. In their own words, as read on 2 November 2026:

  • Simple checks only. The Python README: “Don’t expect this to bypass anything but the simplest of bot detection methods. Consider this a proof-of-concept starting point.”
  • Not undetectable. The Node plugin’s README: “It’s probably impossible to prevent all ways to detect headless chromium, but it should be possible to make it so difficult that it becomes cost-prohibitive or triggers too many false-positives to be feasible.”
  • Chromium first. playwright-extra says the stealth plugin “is optimized for chromium based browsers currently”.
  • No persistent contexts yet. The Python package’s to-do list opens with “make this work with playwright.launch_persistent_context”.
  • CAPTCHAs are a separate plugin. The playwright-extra README lists CAPTCHA solving under a different plugin, which hands the challenge to an outside solving service. Neither stealth package’s README mentions hCaptcha.

The Node packages have had no npm release since March 2023. The Python fork is the active one: its 2.0.3 release in April 2026 added the navigator.userAgentData patch. Patchright takes a different route: it is a patched copy of Playwright itself, so you change the import instead of adding a plugin.

Does playwright-stealth get past hCaptcha?

A stealth patch changes what your browser says about itself. It can make a visit look less like automation, and that is one of several things that decide whether a site’s hCaptcha shows a challenge at all; why am I getting hCaptcha covers the others, starting with the reputation of your IP address, which no browser patch changes.

Once a challenge does appear, stealth has nothing more to offer. The site accepts the form only with a valid token in the h-captcha-response field, a P1_ string its server checks with hCaptcha before it lets the request through. Invisible hCaptcha needs that token too, even when no puzzle is shown. A browser patch does not produce one.

Where NoneCap fits

NoneCap is an hCaptcha solving API. Keep stealth for the rest of your Playwright run, and when a page has hCaptcha, send its sitekey and url to NoneCap, get a P1_ token back, put it in the form and submit. Failed, cancelled and expired solves are never charged.

Python · stealth plus an hCaptcha token
import os
import time
import requests
from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth

API = "https://api.nonecap.com/v1/solves"
HEADERS = {"Authorization": f"Bearer {os.environ['NONECAP_KEY']}"}

def solve_hcaptcha(sitekey: str, url: str) -> str:
    r = requests.post(
        API,
        headers=HEADERS,
        params={"wait": 90},
        json={"type": "hcaptcha", "sitekey": sitekey, "url": url},
        timeout=120,
    )
    r.raise_for_status()
    data = r.json()
    while data["status"] in ("pending", "solving"):
        time.sleep(2)
        data = requests.get(f"{API}/{data['id']}", headers=HEADERS, timeout=30).json()
    if data["status"] != "solved":
        raise RuntimeError(f"solve {data['status']}: {data.get('error')}")
    return data["token"]

with Stealth().use_sync(sync_playwright()) as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://your-site.example/signup")

    sitekey = page.locator("[data-sitekey]").first.get_attribute("data-sitekey")
    token = solve_hcaptcha(sitekey, page.url)

    page.evaluate(
        """(token) => {
            for (const name of ['h-captcha-response', 'g-recaptcha-response']) {
                const el = document.querySelector(`textarea[name="${name}"]`);
                if (el) el.value = token;
            }
        }""",
        token,
    )
    page.click("button[type=submit]")
    browser.close()

The Playwright guide has the Node version, invisible and enterprise (rqdata) sitekeys, and how to tell hCaptcha from reCAPTCHA and Turnstile in the DOM. Every field and status is in the API reference. NoneCap solves hCaptcha only; it does not help with reCAPTCHA, Turnstile or other CAPTCHAs.

Solve hCaptcha in Playwright →

Last updated November 2026.

Frequently asked

What is playwright-stealth?
Two things go by the name. On PyPI, playwright-stealth is a Python package that patches a Playwright browser so it reports fewer automation markers, such as navigator.webdriver. In Node, “Playwright stealth” usually means playwright-extra with puppeteer-extra-plugin-stealth, the plugin the Python package was ported from.
How do I install playwright-stealth?
In Python, pip install playwright-stealth, then wrap your Playwright context manager: with Stealth().use_sync(sync_playwright()) as p:. In Node, npm install playwright playwright-extra puppeteer-extra-plugin-stealth, import chromium from playwright-extra and call chromium.use(StealthPlugin()).
Why do I get “cannot import name stealth_sync”?
Version 2.0 of the Python package renamed its API, and older tutorials still show stealth_sync(page) and stealth_async(page). In 2.0.3 the package exports only Stealth and ALL_EVASIONS_DISABLED_KWARGS. Use Stealth().use_sync(...) for everything, or Stealth().apply_stealth_sync(page) (and the _async versions) for one page or context.
Does playwright-stealth work with launch_persistent_context?
Not yet, by its own README: as of version 2.0.3, “make this work with playwright.launch_persistent_context” is the first item on its to-do list. The README explains that the package sometimes reads the user agent first, which is hard to do once a persistent context has launched.
Does playwright-stealth work in Firefox and WebKit?
The Node stealth plugin is Chromium-first: the playwright-extra README says it “is optimized for chromium based browsers currently”. The Python package’s current examples use Chromium as well.
Does playwright-stealth bypass CAPTCHA?
Neither package says it does. The Python README warns: “Don’t expect this to bypass anything but the simplest of bot detection methods.” A stealth patch changes what your browser reports about itself. When a site shows an hCaptcha challenge, the form still needs a valid token in h-captcha-response, and the patch does not provide one. NoneCap returns that token for hCaptcha.
Is it allowed to use stealth and a solver on a site?
That depends on the site and on what you do there. Automate where you are allowed to: your own sites, your tests, and services whose terms permit it. NoneCap’s terms rule out fraud, credential stuffing, spam and unauthorised access. This is not legal advice.

Start solving hCaptcha in minutes.

1,300 free credits on signup. Pay per solve, credits never expire, failed solves never charged.