How puppeteer-extra-plugin-stealth Works and Where It Falls Short
Modern detection examines protocol and network layers the plugin cannot reach.

puppeteer-extra-plugin-stealth works by patching a bounded set of JavaScript-visible automation signals. This article explains those patches and the limits that follow from where they run. The central distinction is between making page properties look ordinary and making an entire browser session coherent. The plugin can help with the first task, but modern detection also examines layers it cannot reach.
What puppeteer-extra-plugin-stealth actually is and what problem it was built to solve
Standard Puppeteer exposes several signs of automation that a site can inspect immediately. These include navigator.webdriver set to true, headless Chrome user-agent strings, missing Chrome API objects, and plugin arrays that look unlike those in a real desktop session. A site can use these checks to spot automation without looking at the rest of the session. The plugin was built to change what those checks see.
The tool is built on puppeteer-extra and uses page.evaluateOnNewDocument() to inject overrides at the start of the page lifecycle, before site scripts can read the targeted properties. Timing is central to that mechanism. An override applied after a detection script reads a property would arrive too late for that check. Calling puppeteer.use(StealthPlugin()) activates all evasion modules automatically with sensible defaults, while enabledEvasions allows individual modules to be added or removed.
What each evasion module patches and why that enumerable list matters
The most critical module, navigator.webdriver, addresses the primary automation signal and is enabled by default. The chrome.runtime module fakes the object absent in headless mode, while chrome.csi and chrome.app add missing Chrome API stubs. These patches concern what the page can find when it inspects those objects.
The navigator.plugins module spoofs the plugin array to make it look like one in a desktop browser, while navigator.languages overrides the blank language lists often found in automated Linux environments. The navigator.permissions module adjusts permission query behavior to match a headed browser, and media.codecs spoofs codec support. Each has a distinct target. The sourceurl module hides the //# sourceURL comment left by injected scripts, while iframe.contentWindow proxies the iframe’s global object to address srcdoc iframe detection.
The webgl.vendor module changes WebGL vendor and renderer strings away from software-renderer defaults. Strings are the scope of that patch. The user-agent-override module sets a realistic User-Agent header and keeps it consistent with navigator.userAgent and Client Hints, because a mismatch is itself a detection signal. This inventory gives the reader a concrete boundary against which to assess any claim of coverage.
Why JavaScript-layer patching cannot hide signals that live below the page
What happens when a detector examines something outside that inventory? The first gap is in the browser’s automation protocol. When Puppeteer connects to Chrome, commands including Runtime.enable, Page.enable, and Network.enable leave artifacts in the browser’s internal state that anti-bot scripts can detect. Runtime.enable alone can identify an automated session. The plugin operates at the page layer rather than the protocol layer, so its overrides do not hide the fact that those commands have been issued.
A second gap appears when inspection moves from a property’s value to its implementation. A detector need not stop at navigator.webdriver. It can inspect the property descriptor and getter. A real browser defines webdriver as a native getter on the Navigator prototype, whereas the plugin’s Object.defineProperty override introduces differences in the configurable flag and the getter’s toString() output. Replacing native implementations with JavaScript stubs also leaves differences in objects and behavior such as chrome.runtime, plugin arrays, and permission queries.
The third gap precedes the page entirely. Anti-bot systems can check TLS fingerprints, including JA3/JA4, before they serve any HTML. A session with an unsuitable fingerprint can be challenged or blocked before the plugin’s patches load. The plugin has no access to that layer.
The fourth gap lies in the rendering engine. Even with --headless=new, headless Chrome can differ from headed Chrome in font metrics, GPU-accelerated canvas output, and screen dimension APIs. The plugin patches some navigator properties and WebGL strings, but it cannot change how the engine produces its output.
How modern detection systems combine these signals into session-level scoring
These gaps do not operate as four independent pass-or-fail tests. Modern detection systems score network, protocol, browser runtime, and behavioral signals together. Passing a check at one layer may earn no partial credit. A runtime that looks ordinary can still look suspicious as part of a profile with unusual protocol or network evidence.
Context adds another source of inconsistency. A datacenter IP paired with a residential-looking locale, timezone, and language can create a coherence failure. A fresh profile with no cookies or storage history on every run also becomes a behavioral pattern. History becomes part of the evidence. Detection models can correlate micro-movements, dwell times, layout shifts, WebGL and Canvas traits, TLS ClientHello, and history consistency across the session.
A script can pass a static fingerprint test page and still be blocked in production. The test and the production system examine different evidence. The plugin improves only the runtime portion of the composite and does not improve the network, protocol, or behavioral layers. A production session can still be statistically unlikely even when each patched property looks fine on its own.
The maintenance freeze that turns a known gap into a widening one
The npm registry lists version 2.11.2 as the latest release, with a date of 1 March 2023. The detection ecosystem has added roughly 36 months of new signatures since that release. Even a patch at the right layer is shaped by the detection checks it was built to handle. A fixed collection of overrides cannot, by itself, track changes in those checks.
A stealth plugin is consequently a snapshot. Its deterioration can also be difficult to notice. A module can silently no-op and leave a flag exposed without producing an error that tells the developer coverage has changed. High download volume has persisted despite the release freeze, leaving a wide gap between expectations and current behavior in production codebases. The analytical concern is not a judgment about inactivity, but the divergence between a frozen implementation and a moving detection target.
What a head-to-head benchmark reveals about where the plugin now sits in the field
A production benchmark of seven stealth-browser libraries provides a concrete comparison, with each run three times from a real residential network. The reported results give nodriver 28 OK / 0 blocked. CloakBrowser and curl_cffi each recorded 26 OK / 2 blocked, while Patchright recorded 25 OK / 3 blocked. Vanilla Playwright without stealth and rebrowser-playwright each recorded 24 OK / 5 blocked. These results hold for the benchmark’s conditions and may differ for other targets or sessions.
The benchmark places JavaScript-patch-only approaches near the bottom of its detection-resistance ranking, with protocol- and engine-level solutions leading. However, the supplied results do not give an OK/blocked row for the plugin. The benchmark gives the plugin’s version as 2.11.2, released 1 March 2023. An exact score cannot be inferred from that detail. The comparison supports the architectural distinction, but it does not establish a numerical margin between the plugin and any named alternative.
That distinction limits what the comparison can answer. It shows how specific implementations fared under the same network conditions, while the earlier analysis explains why it matters which layer they change. The counts do not reveal which individual signal caused each block, and they cannot turn a driver-level intervention into an engine-level one.
Where the plugin still provides genuine value and where it reliably fails
The strongest objection to this diagnosis is also correct. The plugin is not without value. Public pages without aggressive bot management, internal tooling and QA automation in controlled environments, and scraping targets that rely mainly on static JavaScript property checks can still benefit from reduced fingerprint leakage. In those settings, the detector’s scope can overlap with the plugin’s scope.
The boundary changes when a target evaluates session continuity, interaction timing, or dwell patterns. Those signals are outside the plugin’s runtime patches. Sites checking TLS before serving content and platforms inspecting CDP artifacts likewise examine evidence it does not alter. Network signals, including a datacenter IP or proxy mismatch, can also undermine an otherwise clean browser fingerprint. With those checks in place, the plugin can fail outright or work differently depending on the rest of the session.
For developers, the practical lesson is to distinguish a useful baseline from complete protection. Passing a static check does not show that the plugin covers the defenses used in production. Conversely, a failure against a protocol-aware system does not erase the plugin’s usefulness against simpler property checks. Scope is the deciding distinction in both cases. At production scale, that distinction matters even more because the integration can keep working while the protection it was meant to give fails silently.
The two architectural approaches that address the layers the plugin cannot reach
The alternatives divide into patched drivers and patched browsers. They intervene at different layers of the stack. Rebrowser provides drop-in replacements named rebrowser-puppeteer and rebrowser-playwright, preserving the API while applying fork-level patches to CDP artifacts. These include changes to how Runtime.enable and other commands interact with the browser. The aim is to avoid protocol traces.
Patchright also targets protocol leaks in Playwright. Its changes include replacing Runtime.enable with isolated execution contexts, patching Console.enable, and modifying browser launch flags. These interventions address automation fingerprints exposed through CDP, flags, and related vectors. Device realism remains a separate problem, however. Patched drivers do not by themselves address Canvas, WebGL, AudioContext, and font-rendering evidence that asks whether the browser resembles a real device.
Patched browsers instead modify Chromium itself to make fingerprint signals coherent at the engine level. Canvas output, WebGL rendering, AudioContext data, font metrics, screen dimensions, timezone, and locale can then match a single device profile rather than depend on JavaScript stubs over an unchanged engine. This reaches a different set of mechanisms. Engine-level builds can also address TLS matching, CDP connections without Runtime.enable artifacts, and browser-level WebRTC leak prevention. The trade-off is a heavier dependency and less drop-in compatibility than a driver replacement.
Teams that need these pieces to work together at scale can use managed infrastructure. Keeping the browser fleet, proxy routing, TLS alignment, and session management working together adds to the work that the original convenience plugin helped avoid. The migration choice follows from the required layer. For an existing plugin-and-Puppeteer setup, rebrowser-puppeteer offers the minimum-friction move to driver-level CDP fixes through the same API. Where engine-level fingerprint depth is needed, the next step is a patched browser or managed engine-level Chromium build.


