Patchright: setup, Playwright differences and hCaptcha.

Patchright is Playwright with a patched driver for Chromium. How to install it in Python or Node, what it changes, what its README claims, and how to get an hCaptcha token in a Patchright session.

What Patchright is

Patchright is a fork of Microsoft’s Playwright. Its README describes it as “a patched and undetected version of the Playwright Testing and Automation Framework” that “can be used as a drop-in replacement for Playwright”. You keep the Playwright API and change the import. The patches sit in the driver, the part of Playwright that sends commands to the browser, so the page sees fewer of the traces automation leaves behind.

The Patchright packages (versions read from PyPI and npm on 2 November 2026)
PackageInstallLatest versionNeeds
patchright (PyPI)pip install patchright1.64.3, released 25 October 2026Python 3.10+
patchright (npm)npm i patchright1.64.2, published 25 October 2026Node 20+

The project lives in three repositories under Kaliiiiiiiiii-Vinyzu on GitHub: the driver, the Python package and the Node package. A .NET port is run by the community. The active maintainer is Vinyzu, with Kaliiiiiiiiii as co-maintainer, and the code is Apache 2.0. The driver repository dates from November 2023 and had about 5,000 GitHub stars on 2 November 2026. Version numbers follow Playwright’s: Patchright 1.64.x is built on Playwright 1.64, and its README says new versions deploy automatically, with fixes for Playwright changes sometimes taking a few days.

Patchright vs Playwright

Per the Patchright README, these are the differences you will notice:

What Patchright changes, per its README (2 November 2026)
PlaywrightPatchright
BrowsersChromium, Firefox, WebKitChromium-based browsers only
navigator.webdrivertruefalse, through changed launch flags (listed below the table)
CDP Runtime.enableUsedAvoided; scripts run in isolated execution contexts
Consolepage.on("console") firesDisabled: the README says console functionality will not work
page.evaluateRuns in the page’s own JavaScript worldRuns in an isolated world by default; an extra argument picks the page’s world
Closed shadow rootsLocators do not reach insideNormal locators and XPath reach inside
Launch flagsPlaywright’s defaultsOne flag added and five dropped (listed below the table)

The launch flags it changes, per the README: adding the first and dropping the second make navigator.webdriver read false; the others are dropped to avoid popup crashes, to look less like an automation driver, and to allow default apps and extensions.

Patchright launch flags vs Playwright
+ --disable-blink-features=AutomationControlled
- --enable-automation
- --disable-popup-blocking
- --disable-component-update
- --disable-default-apps
- --disable-extensions

We ran Patchright 1.64.3 (Python) and 1.64.2 (Node) next to Playwright 1.64.1 on 2 November 2026, headless, on Chromium 156. With Playwright, navigator.webdriver read true, the page’s console.log reached page.on("console"), and page.evaluate could read a variable the page had set. With Patchright, navigator.webdriver read false, no console event arrived, and the default evaluate returned None for that variable until we asked for the page’s world. A button inside a closed shadow root was found by Patchright’s CSS and XPath locators and by neither in Playwright. In headless mode the user agent still said HeadlessChrome with both libraries; Patchright does not rewrite it.

Install and use Patchright in Python

Shell · pip
pip install patchright
patchright install chromium

Then write Playwright code with the patchright import:

Python · sync
from patchright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com/")
    print(page.title())
    print(page.evaluate("navigator.webdriver"))   # False (True in Playwright)
    browser.close()

The async API is the same, from patchright.async_api:

Python · async
import asyncio
from patchright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.com/")
        print(await page.title())
        await browser.close()

asyncio.run(main())

Moving an existing Playwright project over is a change to the import lines:

Switching from Playwright
- from playwright.sync_api import sync_playwright
+ from patchright.sync_api import sync_playwright

- import { chromium } from 'playwright'
+ import { chromium } from 'patchright'

Install and use Patchright in Node

Shell · npm
npm i patchright
npx patchright install chromium

The example uses top-level await, so save it as an .mjs file. The README’s own example uses require('patchright'), which works too.

Node · patchright
import { chromium } from 'patchright'

const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto('https://example.com/')
console.log(await page.title())
console.log(await page.evaluate(() => navigator.webdriver))   // false
await browser.close()

Reading page variables: the isolated world

Because Patchright runs evaluate in an isolated world by default, your script sees the page’s DOM but not the JavaScript globals the page defines. The README documents an extra option on evaluate, evaluate_handle and evaluate_all to choose the world. In Node 1.64.2 it is the fourth argument, after the options argument the README writes as ...; reading that signature as a third argument throws “Too many arguments”.

Python · the page’s own world
page.evaluate("window.appState")                          # None
page.evaluate("window.appState", isolated_context=False)  # the page's value
Node 1.64.2 · the page’s own world
await page.evaluate(() => window.appState)                              // undefined
await page.evaluate(() => window.appState, undefined, undefined, false)  // the page's value

The setup its README recommends

The quick starts above run headless Chromium, which is what we tested. For the least detectable setup, the README recommends Google Chrome instead of Chromium (patchright install chrome), a persistent profile, a visible window, no fixed viewport, and no custom headers or user agent. We did not run this one, since it opens a visible browser window:

Python · README recommended setup
from patchright.sync_api import sync_playwright

with sync_playwright() as p:
    context = p.chromium.launch_persistent_context(
        user_data_dir="./profile",
        channel="chrome",
        headless=False,
        no_viewport=True,
    )
    page = context.new_page()

What its README claims, and what it says to watch for

  • Detection. “With the right setup, Patchright currently is considered undetectable.” The README then lists the bot checks and test pages it says it passes, among them Cloudflare, Akamai, DataDome, Kasada and CreepJS. hCaptcha is not on the list. We have not tested these claims.
  • Chromium only. “Patchright only patches CHROMIUM based browsers. Firefox and Webkit are not supported.”
  • Not every Playwright test passes. “Patchright passes most, but not all the Playwright tests.” The maintainers keep a list of the known failures in the driver repository’s issues.
  • Init scripts go through routes. To run init scripts without Runtime.enable, Patchright injects them into HTML requests with Playwright routes, so they can cause the same bugs as routes, and the README notes they “can be detected by Timing Attacks”.
  • Educational use. The README is published “for educational purposes only” and puts responsibility for misuse on the user.

Does Patchright get past hCaptcha?

A patched driver changes what the browser reveals about automation, and that is one of the 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 driver patch changes.

Once a challenge appears, Patchright does nothing more. 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 patched driver does not produce one.

Get an hCaptcha token in a Patchright session

NoneCap is an hCaptcha solving API. Keep Patchright for the rest of your 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 · Patchright plus an hCaptcha token
import os
import time
import requests
from patchright.sync_api import sync_playwright

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 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)

    # isolated_context=False: the site's data-callback lives in the page's world,
    # which Patchright's default evaluate cannot see.
    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;
            }
            const cb = document.querySelector('[data-callback]')?.getAttribute('data-callback');
            if (cb && typeof window[cb] === 'function') window[cb](token);
        }""",
        token,
        isolated_context=False,
    )
    page.click("button[type=submit]")
    browser.close()

One Patchright detail matters here. Many hCaptcha forms submit from a data-callback function the page defines, and Patchright’s default evaluate cannot see it. In our test with a stub token, the default call filled the textarea but never called the callback; with isolated_context=False it did both. In Node, pass false as the fourth argument of page.evaluate.

The Playwright guide has the full Node version, enterprise (rqdata) sitekeys, and how to tell hCaptcha from reCAPTCHA and Turnstile in the DOM; it applies to Patchright once you change the import. If you use a stealth plugin instead of a patched driver, see Playwright stealth. 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 Patchright?
Patchright is a patched build of Microsoft’s Playwright that changes how the driver talks to Chromium so the browser shows fewer automation markers. Its README calls it “a drop-in replacement for Playwright”. It ships as patchright on PyPI and npm, with a community-run .NET port, and is maintained by Vinyzu with Kaliiiiiiiiii as co-maintainer under the Apache 2.0 licence.
How do I install Patchright in Python?
Run pip install patchright, then patchright install chromium to download the browser. In your code, import from patchright.sync_api or patchright.async_api instead of playwright; the rest of the Playwright API stays the same.
What is the difference between Patchright and Playwright?
Patchright avoids the CDP Runtime.enable command, disables the console, changes Chromium’s launch flags so navigator.webdriver reads false, runs evaluate in an isolated world by default, and can reach elements inside closed shadow roots. It supports only Chromium-based browsers, where Playwright also drives Firefox and WebKit.
Why does page.on("console") never fire in Patchright?
Patchright disables the Console API on purpose; its README says console functionality will not work. In our run, a page that called console.log produced no console events under Patchright and one under Playwright. Log through your own script code or the network instead.
Why does page.evaluate return None or undefined for my page’s variables?
Patchright runs evaluate in an isolated world by default, so globals the page defines are not visible there; the DOM is. Pass isolated_context=False in Python, or false as the fourth argument in Node 1.64.2, to run in the page’s own world.
Does Patchright work with Firefox or WebKit?
No. The README says: “Patchright only patches CHROMIUM based browsers. Firefox and Webkit are not supported.” It recommends Google Chrome over Chromium, installed with patchright install chrome and opened with channel="chrome".
Does Patchright bypass hCaptcha?
Its README lists the bot checks it says it passes, and hCaptcha is not on that list. Whatever a patched driver changes about the browser, it does not return an hCaptcha token. When a site shows hCaptcha, the form still needs a valid token in h-captcha-response. NoneCap returns that token, and you set it from your Patchright script.
Is it allowed to use Patchright 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.