Does puppeteer-extra Stealth Still Work in 2026? (The Runtime.enable Leak)
For most of the last decade, the answer to “how do I hide a headless browser?” was a one-liner: install puppeteer-extra-plugin-stealth, call puppeteer.use(Stealth()), and move on. It patched away the obvious tells and, against most sites, it just worked.
In 2026 the honest answer is more nuanced. The plugin still patches the signals it always patched, and on simple sites that is plenty. But the project has been effectively unmaintained for years, modern anti-bot vendors have moved their detection a layer below where the plugin operates, and there is one specific leak — Runtime.enable — that no amount of in-page JavaScript patching can close.
This guide explains what the stealth plugin still fixes, why the Runtime.enable CDP leak defeats it, and which of the 2026 alternatives — rebrowser-patches, puppeteer-real-browser, patchright, and nodriver — you should actually reach for, with runnable code throughout.
TL;DR: The stealth plugin still works against simple sites — it hides navigator.webdriver, the missing window.chrome, headless WebGL strings, and UA mismatches. It does not fix the Runtime.enable CDP leak, where Puppeteer and Playwright call Runtime.enable over the DevTools Protocol and page scripts can detect the resulting execution context. The plugin is also largely unmaintained and adds its own artifacts. On hard targets, switch to a CDP-level fix: rebrowser-patches (Node drop-in), patchright (Playwright drop-in), puppeteer-real-browser, or nodriver (Python). Or skip the arms race with a SERP API.
What the stealth plugin still fixes (and what it never did)
The stealth plugin is a bundle of small evasion modules, each patching one signal that a vanilla headless Chromium leaks. Understanding what is in that bundle tells you exactly where its ceiling is.
Its modules cover the classic, in-page tells: it sets navigator.webdriver to false, recreates the window.chrome object that headless drops, fixes the navigator.plugins and navigator.languages arrays, masks the WebGL vendor and renderer strings, and patches navigator.permissions so a notification query does not betray the browser. These are real signals, and patching them is genuinely useful.
Here is a detection probe you can run yourself to see the most basic of those signals. Open any site under your automation and evaluate this in the page context — on a stealthed browser the first line should read false or undefined:
// Run inside the page (page.evaluate in Puppeteer/Playwright,
// or paste into a real DevTools console for comparison).
const signals = {
webdriver: navigator.webdriver, // true on vanilla headless
hasChrome: !!window.chrome, // false on vanilla headless
pluginsLen: navigator.plugins.length, // 0 on vanilla headless
languages: navigator.languages, // [] / odd on headless
webglVendor: (() => {
const gl = document.createElement('canvas').getContext('webgl');
const ext = gl && gl.getExtension('WEBGL_debug_renderer_info');
return ext ? gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) : null;
})(), // "Google Inc." leaks SwiftShader
};
console.log(JSON.stringify(signals, null, 2));
What the plugin never addressed is anything below the JavaScript layer. It cannot change your TLS fingerprint, your IP reputation, your real mouse-and-timing behaviour, or — crucially — the way the automation protocol itself talks to the browser. We catalogue the full set of in-page signals in how to beat headless Chrome detection; the point here is that stealth only ever covered one slice of that list, and modern vendors have built their detection around the slices it ignores.
There is a second, subtler problem: the plugin's patches are themselves detectable. A property that has been redefined leaves a different toString() signature or descriptor than a native one, so an anti-bot script that checks how a function stringifies can sometimes tell that something monkey-patched it. In other words, an outdated stealth bundle can add tells even as it removes others.
The Runtime.enable CDP leak, explained
This is the leak that reframed the whole conversation. To understand it you have to remember how Puppeteer and Playwright actually drive a browser: not by injecting code, but by speaking the Chrome DevTools Protocol (CDP) to it over a WebSocket.
For your script to call page.evaluate(), query selectors, or read the DOM, the driver needs a handle on each frame's JavaScript execution context. The standard way it obtains that handle is to send the CDP command Runtime.enable. That command tells Chromium to start emitting Runtime.executionContextCreated events — and that is the tell.
The problem is that enabling the Runtime domain has an observable side effect inside the page. The classic detection trick is to create an Error, define a getter on its stack property, and log it. When the CDP Runtime domain is enabled, the act of the console serializing that object triggers behaviour the getter can observe — effectively, page JavaScript can tell that a debugger-style consumer is attached. A simplified version of what anti-bot scripts watch for looks like this:
// A page-side probe an anti-bot script can run.
// When the CDP Runtime domain is enabled, accessing the
// error's stack during console serialization is observable.
let detected = false;
const bait = new Error();
Object.defineProperty(bait, 'stack', {
get() { detected = true; return ''; },
});
console.debug(bait); // serialization touches .stack
console.log('automation attached:', detected);
Because the stealth plugin works by running JavaScript inside the page, it sits on the wrong side of this problem entirely. The leak is created by the protocol layer underneath the page, before any of the plugin's evasions get a chance to run. No amount of patching navigator can hide a side effect produced by the CDP transport itself. That is the core reason the plugin alone is no longer enough on hardened targets, and the canonical write-up of the mechanism and its fixes is rebrowser's deep-dive on how to fix Runtime.enable CDP detection.
The fix has to happen at the same layer as the leak. Instead of blindly calling Runtime.enable, a patched runtime obtains execution-context handles a different way — for example by creating an isolated world per frame and resolving contexts lazily — so the giveaway event is never emitted on the main world. You cannot do that from a plugin loaded into the page; you have to patch the driver or the browser.
The 2026 alternatives
Because the fix lives below the page, the modern tools are not plugins — they are patched runtimes and frameworks. Four are worth knowing, and they differ mostly in which stack they bolt onto.
rebrowser-patches (Node + Puppeteer/Playwright)
If you have an existing Puppeteer codebase, rebrowser-patches is the closest thing to a drop-in fix. It patches the Puppeteer (or Playwright) library so it no longer leaks Runtime.enable on the main world, while your own scripts stay exactly the same. You install a patched build and keep writing the Puppeteer you already know:
# Install the patched Puppeteer build, then keep your code as-is.
# (rebrowser publishes pre-patched packages; you can also run the
# patcher against your own puppeteer install.)
npm install rebrowser-puppeteer-core puppeteer-extra puppeteer-extra-plugin-stealth
// index.js — same Puppeteer API, leak-free transport underneath
const puppeteer = require('puppeteer-extra');
const Stealth = require('puppeteer-extra-plugin-stealth');
puppeteer.use(Stealth()); // still handles the in-page signals
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-blink-features=AutomationControlled'],
});
const page = await browser.newPage();
// With the rebrowser patch active, Runtime.enable is no longer
// called on the main world, so page.evaluate stays usable
// without emitting the detectable execution-context event.
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const title = await page.evaluate(() => document.title);
console.log(title);
await browser.close();
})();
The neat part is that rebrowser-patches and the stealth plugin are complementary, not rivals: the patch closes the CDP leak, and you can still let the (in-page) stealth modules cover the classic signals. It is the lowest-effort upgrade path for a Node team.
puppeteer-real-browser (Node)
If you would rather adopt a maintained wrapper than patch your dependency tree, puppeteer-real-browser bundles connection-level evasions with conveniences like a Cloudflare turnstile click helper. It launches a real Chrome with the leaky calls handled and hands you a normal page object:
// npm install puppeteer-real-browser
const { connect } = require('puppeteer-real-browser');
(async () => {
const { browser, page } = await connect({
headless: false,
turnstile: true, // auto-handle Cloudflare turnstile widgets
args: ['--no-sandbox'],
});
// Drive it like normal Puppeteer — do a Google search and read titles.
await page.goto(
'https://www.google.com/search?q=' + encodeURIComponent('serp api'),
{ waitUntil: 'domcontentloaded' }
);
const results = await page.evaluate(() =>
[...document.querySelectorAll('a h3')].slice(0, 5).map((h) => ({
title: h.innerText,
url: h.closest('a').href,
}))
);
console.log(results);
await browser.close();
})();
patchright (Python or Node + Playwright)
For Playwright users there is patchright, a patched, drop-in replacement that closes the same Runtime.enable class of leaks while keeping the Playwright API. You change one import and your existing Playwright scripts run unchanged:
# pip install patchright && patchright install chromium
# Drop-in: import from patchright instead of playwright.
from patchright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
page = browser.new_page()
# A Google search with a leak-patched Playwright runtime.
page.goto("https://www.google.com/search?q=serp+api",
wait_until="domcontentloaded")
for h in page.query_selector_all("a h3")[:5]:
print(h.inner_text())
browser.close()
nodriver (Python)
The fourth option takes a different route. nodriver (the successor to undetected-chromedriver) talks CDP directly and is built from the ground up to avoid the leaky calls rather than patch around them, so there is no Selenium or webdriver layer to detect at all. It is Python-first and async, and trades the familiar Playwright/Puppeteer API for a leaner, lower-level one. If you are starting a Python project fresh and want maximum stealth, it belongs on the shortlist alongside patchright.
A decision guide
None of these tools is universally “best” — the right pick depends on your stack and how hard your targets defend themselves. This table maps each option to what it actually solves.
| Tool | What it fixes | Maintained? | When to use it |
|---|---|---|---|
| puppeteer-extra-plugin-stealth | Classic in-page signals (navigator.webdriver, window.chrome, WebGL, UA) | Largely unmaintained | Simple / mid-tier sites; pair it with a CDP fix for hard ones |
| rebrowser-patches | The Runtime.enable CDP leak, at the driver layer | Active | You have an existing Node/Puppeteer codebase and want a near drop-in fix |
| puppeteer-real-browser | Connection-level leaks + Cloudflare turnstile helper | Active | Node, you want a maintained wrapper and turnstile convenience |
| patchright | The same CDP-detection class, as a Playwright drop-in | Active | You are on Playwright (Python or Node) and want one-import migration |
| nodriver | Avoids leaky CDP calls entirely (no webdriver layer) | Active | New Python project, maximum stealth, comfortable with a lower-level API |
A pattern falls out of that table. On a Node/Puppeteer stack, keep your code and add rebrowser-patches (optionally still running stealth for the in-page signals). On Playwright, switch the import to patchright. On a green-field Python project, evaluate nodriver. And on anything genuinely simple, the plain stealth plugin is still fine — do not over-engineer.
Whichever runtime you choose, remember it only solves the browser-fingerprint half of detection. A patched runtime behind a flagged datacenter IP still gets blocked, because TLS and IP reputation are checked before any JavaScript runs — the territory covered in bypassing Cloudflare and DataDome. And if you are weighing the whole automation stack rather than just the stealth layer, our comparison of Playwright vs Puppeteer vs Selenium for SERP scraping walks through which base library to standardise on before you bolt patches onto it.
The honest verdict
So — does puppeteer-extra-plugin-stealth still work in 2026? Yes and no, and the nuance matters more than the headline.
Yes, for the large majority of the web. Most sites that bother checking at all only look for navigator.webdriver, a missing window.chrome, or a headless user agent — exactly the signals the plugin patches. For that tier it remains a perfectly good, two-line answer, and there is no reason to add complexity you will not use.
No, for hardened targets running modern anti-bot systems. There the plugin's age works against you twice: it does nothing about the Runtime.enable leak that exposes the CDP transport, and its own monkey-patches can be fingerprinted as artifacts. On those sites you want a runtime that fixes the leak at the protocol layer — rebrowser-patches, patchright, puppeteer-real-browser, or nodriver — and you treat the stealth plugin, if you keep it, as a helper for the in-page signals only. The upstream puppeteer-extra project remains the place to track its status.
The broader truth is that browser stealth is an arms race with no finish line. Every patched runtime works until the vendor it is hiding from ships a new probe, and then you patch again. That treadmill is fine if scraping is your product; it is a poor use of time if search data is just an input to something else. When the page-rendering and detection game stops being worth running yourself, the techniques in scraping Google for free and the trade-offs in scraping Google without getting blocked in 2026 are the same calculus that points many teams toward a managed API that absorbs the whole problem.
Stop patching runtimes. Get clean SERP JSON instead.
Serpent's SERP API returns clean JSON from Google, Bing, Yahoo & DuckDuckGo — no proxies, no CAPTCHAs, no parser maintenance. Get 10 free searches on signup, then pay-as-you-go from $0.03 per 10,000 searches at scale, no subscription.
Get Your Free API KeyExplore: SERP API · Pricing · Playground
FAQ
Is the puppeteer stealth plugin dead in 2026?
Not dead, but no longer enough on its own. The puppeteer-extra-plugin-stealth project has seen very little maintenance for years, and its patches target an older generation of detection signals. It still works fine against simple and mid-tier sites, but against modern anti-bot systems it actually adds new tells and does nothing about the Runtime.enable CDP leak, so on hard targets you now reach for a patched runtime instead.
What exactly is the Runtime.enable leak?
Puppeteer and Playwright drive the browser over the Chrome DevTools Protocol, and to expose page objects they call Runtime.enable on every frame. That command fires a Runtime.executionContextCreated event that page JavaScript can detect, so an anti-bot script can trip a debugger or watch for the context to know an automation tool is attached. The stealth plugin does not touch this because it patches JavaScript inside the page, not the CDP layer underneath it.
What is the best puppeteer-extra alternative now?
There is no single winner. For a Node and Puppeteer codebase, rebrowser-patches is the closest drop-in fix because it specifically closes the Runtime.enable leak at the CDP level while you keep your existing scripts. If you can move stacks, patchright is a patched Playwright drop-in, puppeteer-real-browser bundles connection-level fixes plus a Cloudflare turnstile helper, and nodriver is a CDP-only Python framework that avoids the leaky calls entirely.
Does stealth still help against simple sites?
Yes. For the large majority of sites that only check navigator.webdriver, a missing window.chrome object, or an obvious headless user agent, the stealth plugin patches exactly those and is perfectly adequate. The case for replacing it only appears on sites running modern fingerprinting and CDP-detection, where the plugin's gaps and its own added artifacts start to cost you. Match the tool to the target rather than always reaching for the heaviest option.



