---
title: "Puppeteer Stealth: Setup, Tests and Limits | Scrape.ly"
description: "Puppeteer stealth in 2026: maintenance status, tested setup with Puppeteer 25, what the plugin patches, the mismatches it creates and when it's not enough."
url: https://scrape.ly/blog/puppeteer-stealth
markdown: https://scrape.ly/blog/puppeteer-stealth.md
---

[Guides](https://scrape.ly/blog/category/guides.md)

# Puppeteer Stealth: Setup, What It Patches, and Where It Stops

Puppeteer stealth is puppeteer-extra-plugin-stealth, a plugin that hides the signs of a headless Chrome. Its maintenance status in October 2026, tested setup with Puppeteer 25, before-and-after results, the header mismatches it creates, a Crawlee setup and the hosted alternative.

[Scrape.ly Team The team behind Scrape.ly](https://scrape.ly/blog/author/scrapely-team.md)

- Published

  Oct 26, 2026

- Reading time

  8 min read

![Pixel-art robot with an orange >\_ on its terminal head, disguised in a dark fedora, a raised collar and a cloak, pulls a data card out of a browser window showing the green Puppeteer logo and drops it into a crate](https://scrape.ly/media/puppeteer-stealth.png?w=1080)

Puppeteer stealth is `puppeteer-extra-plugin-stealth`, a plugin for `puppeteer-extra` that patches headless Chrome so page scripts can't easily tell it's automated. It still works with today's Puppeteer, but you should know up front that it's unmaintained: its last release, 2.11.2, shipped on 1 March 2023.

Below: the plugin's status as checked on npm and GitHub on 26 October 2026, a setup we tested with Puppeteer 25.16.0 and Chrome 156, what each evasion patches, before-and-after results in both of Chrome's headless modes, the header mismatches the plugin introduces and how to fix them, how to use it with Crawlee, and the hosted alternative that makes it unnecessary.

## puppeteer-extra-plugin-stealth at a glance

|              |                                                                                                                                            |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| Package      | `puppeteer-extra-plugin-stealth` 2.11.2, used with `puppeteer-extra` 3.3.6                                                                 |
| Last release | 1 March 2023 (both packages)                                                                                                               |
| Repository   | [berstend/puppeteer-extra](https://github.com/berstend/puppeteer-extra), a monorepo; last push 18 July 2024, 277 open issues, not archived |
| Downloads    | 769,084 in the week of 16 to 22 October 2026 (npm)                                                                                         |
| License      | MIT                                                                                                                                        |
| Evasions     | 17 modules                                                                                                                                 |
| Works with   | Puppeteer, and Playwright through `playwright-extra`                                                                                       |

So, is Puppeteer stealth still maintained? Not actively. Nobody has published a release in more than three and a half years, while Chrome has shipped dozens of versions. It's still downloaded hundreds of thousands of times a week, and as our test shows, its patches still take effect. Treat it as a useful starting point you'll have to watch, not as a maintained product.

## Install and set up Puppeteer stealth

```shell
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
```

`puppeteer-extra` wraps Puppeteer and adds a plugin system; you import it instead of `puppeteer`, register the plugin, and launch as usual:

```javascript
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteer.use(StealthPlugin());

const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle2' });
await new Promise((r) => setTimeout(r, 6000));

const result = await page.evaluate(() => ({
  webdriver: navigator.webdriver,
  passed: document.querySelectorAll('td.passed').length,
  failedOrWarn: document.querySelectorAll('td.failed, td.warn').length,
}));
console.log(result);
await browser.close();
```

[bot.sannysoft.com](https://bot.sannysoft.com/) is a public test page that runs common headless checks and marks each row as passed, warned or failed. The six-second wait matters: the page fills its second table late, and with a three-second wait we sometimes counted only 11 rows instead of 31. The examples use ES modules (`"type": "module"` in `package.json`); in a CommonJS project, `require('puppeteer-extra')` and `require('puppeteer-extra-plugin-stealth')` load the same code.

To check your own setup, run this script, count the failed rows, then compare the `Accept-Language` header, `navigator.languages` and the time zone with your proxy's country. Re-run both checks after every Puppeteer or Chrome upgrade.

## What the stealth plugin patches

The plugin is a bundle of 17 evasion modules. Each overwrites one thing a headless Chrome gets wrong, before any page script runs:

| Evasion                                                                                                                  | What it fixes                                                                         |
| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| `navigator.webdriver`                                                                                                    | The automation flag that is `true` in every automated Chrome                          |
| `user-agent-override`                                                                                                    | Removes "HeadlessChrome" from the User-Agent, sets `Accept-Language` and client hints |
| `chrome.app`, `chrome.csi`, `chrome.loadTimes`, `chrome.runtime`                                                         | Chrome-only objects that the headless shell lacks                                     |
| `navigator.plugins`, `navigator.languages`, `navigator.vendor`, `navigator.hardwareConcurrency`, `navigator.permissions` | Browser APIs that return empty or odd values in headless mode                         |
| `webgl.vendor`                                                                                                           | The software GPU name                                                                 |
| `media.codecs`                                                                                                           | Codec support that differs from regular Chrome                                        |
| `window.outerdimensions`                                                                                                 | A window with no outer size                                                           |
| `iframe.contentWindow`                                                                                                   | Frame-based headless checks                                                           |
| `defaultArgs`                                                                                                            | Launch flags a normal browser doesn't use                                             |
| `sourceurl`                                                                                                              | Traces Puppeteer leaves in script URLs                                                |

Everything here happens in JavaScript and request headers. Nothing touches your IP address, your TLS handshake or how your script moves through a site.

## What our test showed

We ran the script above on 26 October 2026 on macOS, with Puppeteer 25.16.0 driving Chrome for Testing 156, once without the plugin and once with it, in both of Chrome's headless modes: the new headless mode (`headless: true`, Puppeteer's default) and the old headless shell (`headless: 'shell'`).

| Check                                   | New headless | New headless + stealth | Headless shell | Headless shell + stealth |
| --------------------------------------- | ------------ | ---------------------- | -------------- | ------------------------ |
| `navigator.webdriver`                   | `true`       | `false`                | `true`         | `false`                  |
| "HeadlessChrome" in User-Agent          | Yes          | No                     | Yes            | No                       |
| `navigator.plugins.length`              | 5            | 5                      | 0              | 3                        |
| `window.chrome`                         | present      | present                | missing        | present                  |
| bot.sannysoft.com rows failed or warned | 4            | 0                      | 13             | 0                        |

Two takeaways. First, the plugin does what it says: zero flagged rows in both modes. Second, Chrome's new headless mode already does much of the work. Without any plugin it failed only four checks (two User-Agent tests, WebDriver and a memory test), against 13 for the shell. If you still launch with `headless: 'shell'` or an old Puppeteer, switching to the default headless mode is the cheapest improvement you can make.

## The mismatches stealth creates

Passing a test page means the JavaScript surface looks normal. It doesn't mean every signal agrees, and the plugin introduces a few disagreements of its own. We loaded a header echo service with each setup and compared what Chrome sent:

| Signal                   | New headless, no plugin                   | New headless + stealth                                               |
| ------------------------ | ----------------------------------------- | -------------------------------------------------------------------- |
| `Accept-Language` header | `de-DE,de;q=0.9` (the machine's language) | `en-US,en;q=0.9`                                                     |
| `navigator.languages`    | `de-DE`                                   | `en-US,en`                                                           |
| Time zone                | `Europe/Zurich`                           | `Europe/Zurich`                                                      |
| `Sec-CH-UA` header       | `"Not:A-Brand";v="8", "Chromium";v="156"` | `" Not A;Brand";v="99", "Chromium";v="156", "Google Chrome";v="156"` |

The language now claims US English while the time zone stays in Switzerland. And the plugin's `Sec-CH-UA` brand string uses an older format with version 99, not what this Chrome 156 sends on its own. A detector that knows what each Chrome version really sends can spot both.

You can fix the language half. Disable the two default evasions that set it and register them again with your own values, then set the time zone to match your proxy:

```javascript
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import UserAgentOverride from 'puppeteer-extra-plugin-stealth/evasions/user-agent-override/index.js';
import NavigatorLanguages from 'puppeteer-extra-plugin-stealth/evasions/navigator.languages/index.js';

const stealth = StealthPlugin();
stealth.enabledEvasions.delete('user-agent-override');
stealth.enabledEvasions.delete('navigator.languages');
puppeteer.use(stealth);
puppeteer.use(UserAgentOverride({ locale: 'de-DE,de' }));
puppeteer.use(NavigatorLanguages({ languages: ['de-DE', 'de'] }));

const browser = await puppeteer.launch({
  headless: true,
  // args: ['--proxy-server=http://proxy.example:8000'],
});
const page = await browser.newPage();
await page.emulateTimezone('Europe/Berlin');
await page.goto('https://httpbin.org/headers');
const { headers } = JSON.parse(await page.evaluate(() => document.body.innerText));
console.log({
  acceptLanguage: headers['Accept-Language'],
  languages: await page.evaluate(() => navigator.languages),
  timeZone: await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone),
  webdriver: await page.evaluate(() => navigator.webdriver),
});
await browser.close();
```

It printed `acceptLanguage: 'de-DE,de;q=0.9'`, `languages: [ 'de-DE', 'de' ]`, `timeZone: 'Europe/Berlin'` and `webdriver: false`: one consistent German visitor, ready for a German proxy. The same `enabledEvasions.delete()` call turns off any evasion that breaks a site you need. The brand string has no option; it's hard-coded in a plugin that hasn't shipped since 2023.

## Puppeteer stealth with Crawlee

[Crawlee](https://crawlee.dev/) adds queues, retries, sessions and proxy rotation on top of Puppeteer, and it accepts `puppeteer-extra` as its launcher:

```javascript
import { PuppeteerCrawler } from 'crawlee';
import puppeteerExtra from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';

puppeteerExtra.use(StealthPlugin());

const crawler = new PuppeteerCrawler({
    launchContext: { launcher: puppeteerExtra, launchOptions: { headless: true } },
    maxRequestsPerCrawl: 3,
    async requestHandler({ page, request, enqueueLinks }) {
        const webdriver = await page.evaluate(() => navigator.webdriver);
        console.log(request.url, await page.title(), { webdriver });
        await enqueueLinks({ selector: 'li.next a' });
    },
});

await crawler.run(['https://books.toscrape.com/']);
```

With Crawlee 3.18.2 it crawled the first three catalog pages of the books.toscrape.com sandbox, each with `{ webdriver: false }`. Set `headless: true` explicitly in `launchOptions`; in our test the crawler stalled at start without it. Crawlee also brings the next layer of anti-blocking, proxy rotation with sessions, which our guide to [rotating proxies for web scraping](https://scrape.ly/blog/rotating-proxies-for-web-scraping.md) covers with tested code.

## Where Puppeteer stealth stops

The plugin's author is candid about it in the README: "It's probably impossible to prevent all ways to detect headless chromium", and the project calls it "a rather interesting cat and mouse game". Here is what stealth can't fix:

- **Your IP address.** A scraper on a cloud server sends every request from a datacenter range. No JavaScript patch changes that; you need good proxies, ideally residential on protected sites.
- **Your TLS handshake.** Anti-bot systems fingerprint the TLS connection itself, before any page script runs.
- **CAPTCHAs.** Stealth lowers the odds of a challenge; it doesn't solve one. Our [CAPTCHA bypass guide](https://scrape.ly/blog/captcha-solving-for-scraping.md) covers prevention and what solving services involve.
- **Behavior.** Request rate, navigation order and session patterns are scored separately from the fingerprint.
- **Time.** Detection keeps changing, and this plugin patches the Chrome of 2023. Re-test after every Puppeteer or Chrome upgrade.

If you use Playwright, the same plugin works through `playwright-extra`, and Python has its own package; see [Playwright stealth](https://scrape.ly/blog/playwright-stealth.md). For the broader picture, read [what a headless browser is](https://scrape.ly/blog/what-is-a-headless-browser.md) and [web scraping without getting blocked](https://scrape.ly/blog/web-scraping-without-getting-blocked.md).

## The hosted alternative: Puppeteer on the Scrape.ly browser

Instead of patching headless Chrome yourself and keeping the patches current, run your Puppeteer code on a browser built to look like a visitor. The [Scrape.ly browser](https://scrape.ly/docs/browser.md) is a hosted Chromium with anti-bot fingerprints, its own residential proxy and Cloudflare Turnstile solving, on by default. You don't need a stealth plugin.

You turn it on per Actor (**Settings** → **Scrapely Browser**) and rebuild. From then on every `puppeteer.launch()` and every Crawlee `PuppeteerCrawler` in the Actor connects to it over CDP, without code changes. To pick the location and operating system the fingerprint presents, pass a browser configuration to the crawler:

```javascript
import { Actor, PuppeteerCrawler } from 'scrapely';

await Actor.main(async () => {
    const browserConfiguration = await Actor.createBrowserConfiguration({
        countryCode: 'DE',
        os: 'windows',
    });

    const crawler = new PuppeteerCrawler({
        browserConfiguration,
        async requestHandler({ page }) {
            await Actor.pushData({ title: await page.title() });
        },
    });
    await crawler.run(['https://example.com']);
});
```

Time zone and language follow the exit country unless you set them, which solves the mismatch from our test without a line of override code. Code that connects itself can use the run's `CDP_URL` with `puppeteer.connect({ browserWSEndpoint: process.env.CDP_URL })`. It costs $0.45 per browser hour at list price, billed in 30-second blocks plus the browser's traffic, and it counts toward the run's maximum charge. [What is a cloud browser](https://scrape.ly/blog/what-is-a-cloud-browser.md) explains how hosted browsers like this one work.

To start from working code, the [Crawlee + Puppeteer template](https://scrape.ly/templates/crawlee-puppeteer-chrome.md) gives you a project you can push as an Actor. And if your target is a popular site, check the [Scrape.ly Store](https://scrape.ly/store.md) first: 570+ ready-made Actors, maintained by their publishers. For crawling public pages into text, the [Website Content Crawler](https://scrape.ly/store/querystone/website-content-crawler.md) fetches over HTTP with a Chrome TLS fingerprint and opens Chrome only when a page needs JavaScript.

Scrape.ly Store Skip the stealth plugin Crawl public pages to clean text with a Chrome TLS fingerprint and Chrome only when needed: $0.50 per 1,000 pages plus platform usage, on $5 of free monthly credit.

[Try it free](https://scrape.ly/store/querystone/website-content-crawler.md) [Browse the Store](https://scrape.ly/store.md)

## FAQ

### Is puppeteer-extra-plugin-stealth still maintained?

Not actively. The latest version, 2.11.2, was published on 1 March 2023, and the repository's last push was on 18 July 2024, with 277 open issues. It isn't archived and still works, but no one is updating its patches for new Chrome versions.

### Does Puppeteer stealth work with Puppeteer 25?

Yes. In our 26 October 2026 test, Puppeteer 25.16.0 with `puppeteer-extra` 3.3.6 and the stealth plugin passed every check on bot.sannysoft.com with Chrome 156, in both the new headless mode and the headless shell.

### Does Puppeteer stealth bypass Cloudflare?

Not reliably. It hides JavaScript tells, but Cloudflare also scores your IP, TLS fingerprint and behavior, and the plugin changes none of those. A hosted browser with a residential IP and Turnstile solving, like the Scrape.ly browser above, is the more reliable route on public pages.

### Do I still need stealth with Chrome's new headless mode?

For most anti-bot checks, yes. The new headless mode removed most of the old tells, but in our test it still reported `navigator.webdriver` as `true` and "HeadlessChrome" in its User-Agent, two of the easiest signals for a page to read. The plugin hides both; a hosted browser like the Scrape.ly browser avoids them without patching.

### Can I turn off a single evasion?

Yes. Call `stealth.enabledEvasions.delete('evasion-name')` before `puppeteer.use(stealth)`, using a name from the table above. That's also how you replace an evasion with a configured copy, as in the language example.

### Is using Puppeteer stealth legal?

Changing how your own browser presents itself isn't illegal in itself. What you scrape and how still matters: stay on public pages, respect robots.txt, the site's terms and rate limits, and avoid personal data you don't need.

About the author

[Scrape.ly Team](https://scrape.ly/blog/author/scrapely-team.md) The team behind Scrape.ly

Scrape.ly is the web data platform from TeraShift GmbH in Switzerland, powered by Evomi: ready-made scrapers in the Scrape.ly Store, a cloud runtime, an API with JavaScript and Python SDKs, proxies, a hosted anti-bot browser and an MCP server for AI agents.

- [Website](https://scrape.ly/index.md)

Keep reading

## More in Guides

- [![Pixel-art robot with an orange >\_ on its terminal head carries a row of data from a web page on the left into a table on the right that is filling up row by row](https://scrape.ly/media/what-is-web-scraping.png?w=750)](https://scrape.ly/blog/what-is-web-scraping.md)
  ### [What Is Web Scraping? How It Works and What It Is Used For](https://scrape.ly/blog/what-is-web-scraping.md)
  [What is web scraping? The automated collection of data from websites into structured records. See how it works step by step with tested Python code, what teams use it for, how it differs from crawling, APIs and screen scraping, and how to do it without code.](https://scrape.ly/blog/what-is-web-scraping.md)
- [![Pixel-art robot with an orange >\_ on its terminal head pulls a data card out of a monitor showing the blue and yellow Python logo beside lines of code, and drops it into a crate](https://scrape.ly/media/web-scraping-python.png?w=750)](https://scrape.ly/blog/web-scraping-python.md)
  ### [Web Scraping With Python: A Complete 2026 Guide](https://scrape.ly/blog/web-scraping-python.md)
  [A complete guide to web scraping with Python: requests and BeautifulSoup, CSS selectors, pagination, CSV and JSON export, polite delays and retries, async httpx, Playwright for JavaScript pages, Scrapy, and how to run it all in the cloud. Every script tested on 23 October 2026.](https://scrape.ly/blog/web-scraping-python.md)
- [![Pixel-art robot with an orange >\_ on its terminal head stands before a pair of balanced golden scales of justice, holding out a data card](https://scrape.ly/media/is-web-scraping-legal.png?w=750)](https://scrape.ly/blog/is-web-scraping-legal.md)
  ### [Is Web Scraping Legal? US, EU and UK Law, Court Cases and a Checklist (2026)](https://scrape.ly/blog/is-web-scraping-legal.md)
  [Is web scraping legal? What US, EU and UK law actually says in 2026, from Van Buren and hiQ v. LinkedIn to Meta and X v. Bright Data, GDPR, database rights, robots.txt and the newest SerpApi rulings, plus a practical checklist.](https://scrape.ly/blog/is-web-scraping-legal.md)

## The internet is full of data. Start owning yours.

Scrape.ly gives you the infrastructure, the tooling, and the team to extract, process, and operationalize web data at any scale. Setup takes minutes.

[Get Started Free](https://my.scrape.ly/register) [Chat with Support](mailto:hello@scrape.ly?subject=Question%20about%20Scrape.ly\&body=Hi%2C%20I%20have%20a%20question%20about%20Scrape.ly.)

No credit card. No setup calls.
