
Playwright Bot Bypass
- 666 installs
- 183 repo stars
- Updated July 24, 2026
- greekr4/playwright-bot-bypass
playwright-bot-bypass is a Claude Code skill that configures stealth Playwright browser sessions to reduce bot detection for developers who scrape, test, or automate protected web flows.
About
playwright-bot-bypass is a Playwright stealth automation skill that ships rebrowser-playwright templates and an A/B test script comparing standard Chromium detection versus stealth mode. Developers install playwright and rebrowser-playwright, run node ab-test.mjs, and reuse createStealthBrowser from scripts/stealth-template.mjs for headless sessions. Reach for playwright-bot-bypass when Cloudflare or fingerprint checks block vanilla Playwright in scrapers, E2E suites, or AI agent browser tools. The repo documents npm install steps and falls back to stealth-only runs when standard Playwright is missing.
- Creates stealth browsers using rebrowser-playwright that pass major detection tests
- Runs A/B comparison between standard Playwright and stealth mode on bot.sannysoft.com
- Provides reusable stealth-template.mjs for consistent evasion across projects
- Outputs detailed detection results for webdriver, renderer, and fingerprinting vectors
- Supports both standard and stealth execution paths with graceful degradation
Playwright Bot Bypass by the numbers
- 666 all-time installs (skills.sh)
- Ranked #362 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 25, 2026 (Skillselion catalog sync)
npx skills add https://github.com/greekr4/playwright-bot-bypass --skill playwright-bot-bypassAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 666 |
|---|---|
| repo stars | ★ 183 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 24, 2026 |
| Repository | greekr4/playwright-bot-bypass ↗ |
How do you run Playwright without bot detection?
Run reliable headless browser sessions that evade bot detection when scraping, testing, or automating web flows with AI agents.
Who is it for?
Developers automating or testing sites that block headless Chromium who already use Playwright and need a stealth launch pattern.
Skip if: Teams that only need public APIs, static HTML fetches, or environments where evading bot checks violates site terms.
When should I use this skill?
User reports Playwright blocked, bot detection, Cloudflare challenges, or asks for stealth headless browser automation.
What you get
Stealth browser launch configs, rebrowser-playwright integration, and A/B detection comparison results.
- stealth-template.mjs browser factory
- ab-test.mjs detection comparison script
Files
Playwright Bot Bypass
Reduce bot detection using rebrowser-playwright + real headed Chrome. Passes fingerprint checkers (bot.sannysoft.com, areyouheadless) and avoids triggering CAPTCHAs on Google. Not a guaranteed bypass for CDP/runtime-aware enterprise bot managers — see "Detection Coverage" for measured results.
Authorized use only. This is for QA, accessibility testing, and research on sites you own or are permitted to test. Respect each site's Terms of Service, robots.txt, and applicable law. Do not use it to bypass paywalls, abuse rate limits, or scrape against a site's stated wishes.How Detection Is Defeated (and by which layer)
Evasion comes from three layers, not one — most of it is the real browser, not hand-written JS:
| Detection Point | Standard Playwright (headless) | Defeated by |
|---|---|---|
CDP / Runtime.enable leak | Present (headless tell) | rebrowser + `REBROWSER_PATCHES_RUNTIME_FIX_MODE` (auto-set) |
window.__pwInitScripts (isPlaywright) | Present | artifact strip (init script deletes it every nav) |
navigator.webdriver | true | rebrowser (reports false; we do NOT delete it — undefined is itself a tell) |
| WebGL Renderer | SwiftShader (software) | `channel:'chrome'` + headed mode (real GPU) |
| User Agent | Contains "HeadlessChrome" | `channel:'chrome'` (real Chrome UA) — no JS override |
| Canvas fingerprint | Software-rendered tell | headed real Chrome (genuine GPU canvas) — no JS noise |
navigator.plugins | Empty array | headed real Chrome (genuine PluginArray) — no JS fake |
navigator.languages | ['en-US'] only | `locale` option (native, worker-consistent — no JS getter) |
Why so little hand-written JS? Across v2.1/v2.2 the old fake-PluginArray, canvas-noise, hardcoded-hardwareConcurrency, permissions-override,webdriverdelete, andnavigator.languagesgetter were all removed — every one created a detectable inconsistency (own-property tells, worker mismatches,undefinedwebdriver, anIllegal invocationcrash). v2.2 keeps exactly two active measures: strip Playwright's__pwInitScriptsartifact, and enable rebrowser's Runtime-fix. Everything else is the genuine, self-consistent real browser. Less faking = more consistent = harder to detect.
Prerequisites
- Node.js 18+ with ESM support (
.mjsfiles) - Google Chrome installed (not just Chromium)
- Headed mode required (
headless: false) — no display = no stealth
Verify Chrome is installed:
# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
# Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" --version
# Linux
google-chrome --versionQuick Start
1. Install
npm init -y && npm install rebrowser-playwright2. Create stealth-test.mjs
import os from 'node:os';
import path from 'node:path';
// Enable rebrowser's Runtime.enable fix BEFORE importing the library.
process.env.REBROWSER_PATCHES_RUNTIME_FIX_MODE ??= 'addBinding';
const { chromium } = await import('rebrowser-playwright');
const browser = await chromium.launch({
headless: false, // headed = real GPU/canvas
channel: 'chrome', // real Chrome = real UA/WebGL/plugins
args: ['--disable-blink-features=AutomationControlled']
});
// `locale` sets navigator.languages + Accept-Language natively & consistently.
const context = await browser.newContext({ locale: 'ko-KR' });
// The ONLY init script: strip Playwright's main-world signature. Touch NOTHING
// on navigator — the real browser's values are already genuine & consistent.
await context.addInitScript(() => {
for (const k of Object.getOwnPropertyNames(window)) {
if (/^__pw|pwInitScripts|playwright/i.test(k)) {
try { delete window[k]; } catch {}
}
}
if (!window.chrome) window.chrome = {};
});
const page = await context.newPage();
try {
await page.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle' });
const out = path.join(os.tmpdir(), 'stealth-test.png');
await page.screenshot({ path: out });
console.log(`Screenshot saved: ${out}`);
} finally {
await browser.close();
}3. Run
node stealth-test.mjsUsing the Template (Recommended)
The scripts/stealth-template.mjs provides a reusable factory with all patches pre-applied:
import { createStealthBrowser, humanDelay, humanType, simulateMouseMovement } from './scripts/stealth-template.mjs';
const { browser, page } = await createStealthBrowser();
try {
await page.goto('https://example.com');
// Human-like mouse movement (avoids Cloudflare Turnstile)
await simulateMouseMovement(page);
// Human-like typing instead of instant fill
await humanType(page, 'input[name="q"]', 'search query');
await humanDelay(300, 800);
} finally {
await browser.close();
}Template Options
const { browser, context, page } = await createStealthBrowser({
headless: false, // Required for stealth (default)
viewport: { width: 1280, height: 800 }, // Default
locale: 'ko-KR', // Browser locale (default)
userAgent: null, // Custom UA (optional; default = real Chrome UA)
storageState: './session.json', // Cookie persistence (optional)
proxy: { server: 'http://proxy:8080' }, // Proxy (optional)
noSandbox: false // Opt-in --no-sandbox (Linux root/CI only; it's a bot signal)
});
// Save session for reuse
import { saveSession } from './scripts/stealth-template.mjs';
await saveSession(context, './session.json');What the Template Actually Does
The template (v2.2) keeps the active surface to exactly two measures:
| Measure | Why |
|---|---|
Strip window.__pwInitScripts / __playwright* (init script, every nav) | Removes the deterministic isPlaywright signature detectors key on |
REBROWSER_PATCHES_RUNTIME_FIX_MODE=addBinding (auto-set before import) | Hides the CDP Runtime.enable headless leak |
It touches nothing on navigator. Everything else is delegated to the real browser (channel:'chrome' + headed): genuine User-Agent, WebGL/GPU renderer, canvas fingerprint, PluginArray, self-consistent permission/hardware values, and (via the locale option) native worker-consistent navigator.languages + Accept-Language.
Removed across v2.1/v2.2 (each was net-negative — faked values inconsistently with the real environment): fake navigator.plugins, canvas getImageData noise, hardcoded hardwareConcurrency/deviceMemory, outerWidth/Height offset, the permissions override (crashed with Illegal invocation), the navigator.webdriver delete (made it undefined — a tell), and the navigator.languages getter (own-property + worker-mismatch tells).
Launch arg: --disable-blink-features=AutomationControlled. --no-sandbox is opt-in ({ noSandbox: true }) — it is both a security risk and an automation signal, so it is off by default.
Heavy SPAs (TikTok/IG/FB) may print non-fatal[rebrowser-patches] cannot get worldwarnings underaddBindingmode — the page still loads. Switch toREBROWSER_PATCHES_RUNTIME_FIX_MODE=alwaysIsolatedto silence them if needed.
Scripts
- `scripts/stealth-template.mjs` — Reusable stealth browser factory (all examples import this)
- `scripts/bot-detection-test.mjs` — Verify bypass at bot.sannysoft.com
Examples
- `examples/stealth-google-search.mjs` — Google search without CAPTCHA
- `examples/ab-test.mjs` — Side-by-side detected vs stealth comparison
- `examples/stealth-twitter-scrape.mjs` — Twitter/X profile scraping
Note:ab-test.mjsrequires bothrebrowser-playwrightANDplaywright:
```bash
npm install rebrowser-playwright playwright && npx playwright install chromium
```
All screenshots are saved to the OS temp directory (os.tmpdir()) — /tmp on macOS/Linux, %TEMP% on Windows.
Detection Coverage (measured 2026-06-10, macOS headed Chrome, v2.2)
Honest, tested results. v2.2's artifact-strip + Runtime-fix flipped every fingerprint/automation detector to pass:
| Detector | Result | Note |
|---|---|---|
| bot.sannysoft.com | ✅ all green | webdriver false, real WebGL/UA/plugins |
| arh.antoinevastel.com/bots/areyouheadless | ✅ "not Chrome headless" | |
| hmaker.github.io/selenium-detector | ✅ "Passing" | no chromedriver |
| bot-detector.rebrowser.net | ✅ 0 red | __pwInitScripts stripped, no Runtime leak, webdriver false |
| deviceandbrowserinfo.com/are_you_a_bot | ✅ "You are human!" | isBot:false, isPlaywright:false |
| browserscan.net/bot-detection | ✅ "Normal" | CDP test passes (Runtime-fix) |
| iphey.com | ✅ "Trustworthy" | was "Unreliable" before v2.2 |
| creepjs | ✅ 0% headless / 0% stealth | fuzzy "38% like headless" is not a detection |
| nowsecure.nl (Cloudflare Turnstile) | ⚠️ interactive challenge shown | behavioral/IP gate — not a fingerprint check; not auto-passed |
Reliability: 9/9 repeat runs clean (the __pwInitScripts strip held across every navigation).
How v2.2 achieves this (and the one thing it can't fix)
Two levers, both built into createStealthBrowser(): 1. Artifact strip — an init script deletes window.__pwInitScripts / __playwright* (the deterministic isPlaywright signature) on every navigation. 2. `REBROWSER_PATCHES_RUNTIME_FIX_MODE=addBinding` — set automatically before import; hides the CDP Runtime.enable leak.
The residual leak: window.__playwright_builtins__ is a separate, non-configurable Playwright global that cannot be deleted or redefined. None of the detectors above key on it today, but a future detector could. No rebrowser version (you're on the latest, 1.52.0) removes it — tracked upstream in rebrowser-patches#110, open with no fix.
Takeaway: clean against fingerprint + automation-framework detection. Still not a guaranteed bypass for behavioral/IP systems (Cloudflare Turnstile, DataDome, Kasada) or login walls. For those, add residential IPs + real interaction, or switch engines — patchright (drop-in) or nodriver (structurally avoids the whole CDP/automation-protocol class).
Real community sites (single logged-out load, residential IP, 2026-06-10)
Tested whether real sites bot-block a v2.2 page load (NOT a login judgment):
| Site | Result |
|---|---|
| Reddit, YouTube, Pinterest, Threads, X (direct URL), Quora, TikTok | 🟢 content fully loaded — no bot challenge, no CAPTCHA |
| Instagram, Facebook, LinkedIn | 🟡 content shell loads behind the standard logged-out login modal — NOT a bot block / checkpoint / HTTP 999 |
Key result: none returned a bot challenge or hard block. The "hardest" sites (IG/FB/LinkedIn) served the normal logged-out human experience (a login modal over visible content), which means the stealth passed as a real user. Caveat: this is one load each on a clean residential IP. At volume, IG/FB/LinkedIn enforce datr-cookie aging, HTTP 999, and rate limits — those are IP/account problems, not fingerprint problems, and this skill does not address them. TikTok's signed-API actions (beyond profile view) also still need a warmed session.
Limitations
- Requires
headless: false(headed mode with display) - Needs real Google Chrome installed (
channel: 'chrome') - Some sites may still detect based on behavior patterns — use
humanDelay,humanType,simulateMouseMovement - Does not bypass CAPTCHAs, only prevents triggering them
- TLS/JA3 fingerprint is handled by
channel: 'chrome'(uses real Chrome binary) __pwInitScriptsleak (see Detection Coverage) is unfixable at the skill level — it's inherent to rebrowser-playwright 1.52.0
Python Support
undetected-chromedriver (Recommended)
pip install undetected-chromedriverimport undetected_chromedriver as uc
# Match your Chrome version: check chrome://version
driver = uc.Chrome() # auto-detects version
driver.get("https://www.google.com")
search_box = driver.find_element("name", "q")
search_box.send_keys("your search query")
search_box.submit()Pythonplaywright-stealthonly patches at JS level — WebGL still shows SwiftShader. Useundetected-chromedriverinstead.
Alternative: Call Node.js from Python
import subprocess
result = subprocess.run(['node', 'stealth-script.mjs', query], capture_output=True)Troubleshooting
| Problem | Fix |
|---|---|
ERR_MODULE_NOT_FOUND | Run npm install rebrowser-playwright in the same directory as your script |
| Browser not opening | Verify Chrome is installed (see Prerequisites) |
| WebGL shows SwiftShader | You're effectively headless / GPU-less. Run headed (headless: false) with channel: 'chrome' on a machine with a real GPU; SwiftShader is the software fallback, not an import issue |
| Still getting detected | Add simulateMouseMovement() and humanDelay() between actions |
| Process hangs | Ensure browser.close() is in a finally block |
SyntaxError: await | File must be .mjs or have "type": "module" in package.json |
node_modules/
#!/usr/bin/env node
/**
* A/B Test: Detected vs Stealth
* Compare bot detection between standard playwright and stealth mode
*
* Usage: node ab-test.mjs
* Requires: npm install rebrowser-playwright playwright
*/
import os from 'node:os';
import path from 'node:path';
import { createStealthBrowser } from '../scripts/stealth-template.mjs';
let chromiumStandard;
try {
const pw = await import('playwright');
chromiumStandard = pw.chromium;
} catch {
console.error('Standard "playwright" package not installed.');
console.error('Run: npm install playwright && npx playwright install chromium');
console.error('\nSkipping A side — running stealth-only test.\n');
}
console.log('='.repeat(45));
console.log(' A/B TEST: Detected vs Stealth ');
console.log('='.repeat(45) + '\n');
let browserA, browserB;
try {
// A: Standard Playwright (will be detected)
let resultA = { webdriver: 'skipped', renderer: 'skipped' };
if (chromiumStandard) {
console.log('[A] Starting standard Playwright...');
browserA = await chromiumStandard.launch({
headless: false,
channel: 'chrome',
args: ['--window-position=0,0']
});
const contextA = await browserA.newContext({ viewport: { width: 640, height: 700 } });
const pageA = await contextA.newPage();
await pageA.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle' });
await pageA.waitForSelector('table tr td', { timeout: 10000 });
resultA = await pageA.evaluate(() => {
const get = (name) => {
for (const row of document.querySelectorAll('table tr')) {
if (row.textContent.includes(name)) {
const cells = row.querySelectorAll('td');
return cells.length >= 2 ? cells[1].textContent.trim() : 'N/A';
}
}
return 'N/A';
};
return { webdriver: get('WebDriver'), renderer: get('WebGL Renderer') };
});
await pageA.screenshot({ path: path.join(os.tmpdir(), 'ab-test-detected.png') });
}
// B: Stealth mode (will bypass detection)
console.log('[B] Starting stealth Rebrowser...');
const { browser, page: pageB } = await createStealthBrowser({
viewport: { width: 640, height: 700 }
});
browserB = browser;
await pageB.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle' });
await pageB.waitForSelector('table tr td', { timeout: 10000 });
const resultB = await pageB.evaluate(() => {
const get = (name) => {
for (const row of document.querySelectorAll('table tr')) {
if (row.textContent.includes(name)) {
const cells = row.querySelectorAll('td');
return cells.length >= 2 ? cells[1].textContent.trim() : 'N/A';
}
}
return 'N/A';
};
return { webdriver: get('WebDriver'), renderer: get('WebGL Renderer') };
});
await pageB.screenshot({ path: path.join(os.tmpdir(), 'ab-test-stealth.png') });
// Results
console.log('\n' + '='.repeat(45));
console.log(' RESULTS ');
console.log('='.repeat(45));
console.log(`[A] Standard: WebDriver=${resultA.webdriver}`);
console.log(` Renderer=${resultA.renderer}`);
console.log(`[B] Stealth: WebDriver=${resultB.webdriver}`);
console.log(` Renderer=${resultB.renderer}`);
console.log('='.repeat(45));
console.log(`\nScreenshots: ${path.join(os.tmpdir(), 'ab-test-detected.png')}, ${path.join(os.tmpdir(), 'ab-test-stealth.png')}\n`);
} catch (err) {
console.error('Error:', err.message);
} finally {
await browserA?.close().catch(() => {});
await browserB?.close().catch(() => {});
}
console.log('Done!');
#!/usr/bin/env node
/**
* Stealth Google Search Example
* Demonstrates bot-detection-free Google search with human-like typing
*
* Usage: node stealth-google-search.mjs "search query"
* Requires: npm install rebrowser-playwright
*/
import os from 'node:os';
import path from 'node:path';
import { createStealthBrowser, humanType, humanDelay, simulateMouseMovement } from '../scripts/stealth-template.mjs';
const searchQuery = process.argv[2] || 'Playwright automation';
async function stealthGoogleSearch(query) {
console.log(`Searching Google for: "${query}"\n`);
const { browser, page } = await createStealthBrowser();
try {
await page.goto('https://www.google.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('textarea[name="q"]', { timeout: 10000 });
// Simulate natural mouse movement before typing
await simulateMouseMovement(page);
// Human-like typing instead of instant fill
await humanType(page, 'textarea[name="q"]', query);
await humanDelay(300, 800);
await page.press('textarea[name="q"]', 'Enter');
await page.waitForSelector('div.g, #captcha, #sorry', { timeout: 10000 }).catch(() => {});
const url = page.url();
if (url.includes('sorry')) {
console.log('CAPTCHA detected! Bot detection triggered.');
} else {
console.log('Search successful! No CAPTCHA.');
const results = await page.evaluate(() => {
const items = document.querySelectorAll('div.g');
return Array.from(items).slice(0, 5).map(item => {
const title = item.querySelector('h3')?.textContent || '';
const link = item.querySelector('a')?.href || '';
return { title, link };
});
});
console.log('\nTop Results:');
results.forEach((r, i) => {
console.log(`${i + 1}. ${r.title}`);
console.log(` ${r.link}\n`);
});
}
const outOk = path.join(os.tmpdir(), 'google-search-result.png');
await page.screenshot({ path: outOk });
console.log(`Screenshot saved: ${outOk}\n`);
} catch (err) {
console.error('Error:', err.message);
await page.screenshot({ path: path.join(os.tmpdir(), 'google-search-error.png') }).catch(() => {});
} finally {
await browser.close();
}
}
stealthGoogleSearch(searchQuery).catch(console.error);
#!/usr/bin/env node
/**
* Stealth Twitter/X Scraping Example
* Scrape public Twitter profiles without login
*
* Usage: node stealth-twitter-scrape.mjs [username]
* Requires: npm install rebrowser-playwright
*
* Note: X may require login for some profiles since 2023.
*/
import os from 'node:os';
import path from 'node:path';
import { createStealthBrowser, humanDelay, simulateMouseMovement } from '../scripts/stealth-template.mjs';
const username = process.argv[2] || 'elonmusk';
async function scrapeTwitterProfile(username) {
console.log(`Scraping @${username} profile...\n`);
const { browser, page } = await createStealthBrowser();
try {
await page.goto(`https://x.com/${username}`, { waitUntil: 'domcontentloaded' });
// Simulate human behavior
await simulateMouseMovement(page);
await humanDelay(1000, 2000);
// Wait for tweets to render
await page.waitForSelector('article', { timeout: 15000 }).catch(() => {});
const title = await page.title();
if (!title.includes('@')) {
console.log('Profile not loaded. May require login or be restricted.');
await page.screenshot({ path: path.join(os.tmpdir(), 'twitter-blocked.png') });
return;
}
const data = await page.evaluate(() => {
const articles = document.querySelectorAll('article');
const tweets = Array.from(articles).slice(0, 10).map(article => {
const text = article.innerText.substring(0, 200);
const time = article.querySelector('time')?.getAttribute('datetime') || '';
return { text, time };
});
return {
title: document.title,
url: window.location.href,
tweetCount: articles.length,
tweets
};
});
console.log(`Profile loaded: ${data.title}`);
console.log(`Tweets found: ${data.tweetCount}\n`);
data.tweets.forEach((tweet, i) => {
console.log(`--- Tweet ${i + 1} (${tweet.time}) ---`);
console.log(`${tweet.text.substring(0, 120)}...\n`);
});
const out = path.join(os.tmpdir(), `twitter-${username}.png`);
await page.screenshot({ path: out });
console.log(`Screenshot saved: ${out}\n`);
} catch (err) {
console.error('Error:', err.message);
await page.screenshot({ path: path.join(os.tmpdir(), 'twitter-error.png') }).catch(() => {});
} finally {
await browser.close();
}
}
scrapeTwitterProfile(username).catch(console.error);
{
"name": "playwright-bot-bypass",
"displayName": "playwright-bot-bypass",
"description": "Bypass bot detection using rebrowser-playwright (Node.js) or undetected-chromedriver (Python). Pass bot.sannysoft.com tests and automate Google without triggering CAPTCHA. Includes a public Twitter/X profile-scrape example (note: X often requires login since 2023). Authorized-use only — respect each site's ToS.",
"version": "2.2.0",
"author": "greekr4",
"repository": "https://github.com/greekr4/playwright-bot-bypass",
"license": "MIT",
"keywords": [
"playwright",
"bot-detection",
"stealth",
"captcha-bypass",
"web-scraping",
"automation",
"rebrowser",
"undetected-chromedriver",
"twitter",
"x-scraping",
"nitter-alternative",
"camofox"
],
"categories": [
"automation",
"web-scraping",
"testing"
],
"triggers": [
"bypass bot detection",
"avoid CAPTCHA",
"stealth browser",
"undetected playwright",
"rebrowser-playwright",
"web scraping blocked",
"twitter scraping",
"nitter alternative"
],
"dependencies": {
"node": ["rebrowser-playwright"],
"python": ["undetected-chromedriver"]
},
"platforms": ["macos", "linux", "windows"],
"minClaudeCodeVersion": "1.0.0"
}
{
"name": "playwright-bot-bypass",
"version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-bot-bypass",
"version": "2.1.0",
"dependencies": {
"rebrowser-playwright": "^1.52.0"
},
"optionalDependencies": {
"playwright": "^1.52.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"license": "Apache-2.0",
"optional": true,
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/rebrowser-playwright": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/rebrowser-playwright/-/rebrowser-playwright-1.52.0.tgz",
"integrity": "sha512-UjpqfwmF9+XtOuCCxGQ2ZlLeuSaSv//4Z6ZQgYPsJovz3d7nWodCd2hSRQigAswAUnsPmVwnQUpSn+TLKaKV+A==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "npm:rebrowser-playwright-core@~1.52.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/rebrowser-playwright/node_modules/playwright-core": {
"name": "rebrowser-playwright-core",
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/rebrowser-playwright-core/-/rebrowser-playwright-core-1.52.0.tgz",
"integrity": "sha512-gjrvLNh0RX6B/tg6pWaPNGf+9+z1Jl2EyAh5MXD5xMa2lputGRZ9V2MJ/uofcC5Np3vSOJ3SdVSRqwteC0FjfQ==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
}
}
}
{
"name": "playwright-bot-bypass",
"version": "2.2.0",
"type": "module",
"description": "Bypass bot detection using rebrowser-playwright with stealth patches",
"scripts": {
"test": "node scripts/bot-detection-test.mjs",
"ab-test": "node examples/ab-test.mjs"
},
"dependencies": {
"rebrowser-playwright": "^1.52.0"
},
"optionalDependencies": {
"playwright": "^1.52.0"
}
}
#!/usr/bin/env node
/**
* Bot Detection Test Script
* Tests if the stealth configuration bypasses bot detection
*
* Usage: node bot-detection-test.mjs
* Requires: npm install rebrowser-playwright
*/
import os from 'node:os';
import path from 'node:path';
import { createStealthBrowser } from './stealth-template.mjs';
async function testBotDetection() {
console.log('Bot Detection Test Starting...\n');
const { browser, page } = await createStealthBrowser();
try {
console.log('Navigating to bot.sannysoft.com...\n');
await page.goto('https://bot.sannysoft.com', { waitUntil: 'networkidle' });
await page.waitForSelector('table tr td', { timeout: 10000 });
const results = await page.evaluate(() => {
const getResult = (testName) => {
const rows = document.querySelectorAll('table tr');
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length >= 2 && cells[0].textContent.includes(testName)) {
const resultCell = cells[1];
const isRed = resultCell.style.backgroundColor === 'rgb(255, 102, 102)' ||
resultCell.style.backgroundColor === '#ff6666' ||
resultCell.classList.contains('failed');
return {
value: resultCell.textContent.trim(),
passed: !isRed && !resultCell.textContent.includes('failed')
};
}
}
return { value: 'N/A', passed: true };
};
return {
userAgent: getResult('User Agent'),
webDriver: getResult('WebDriver'),
webDriverAdvanced: getResult('WebDriver Advanced'),
chrome: getResult('Chrome'),
plugins: getResult('Plugins'),
languages: getResult('Languages'),
webglVendor: getResult('WebGL Vendor'),
webglRenderer: getResult('WebGL Renderer')
};
});
console.log('='.repeat(45));
console.log(' BOT DETECTION RESULTS ');
console.log('='.repeat(45) + '\n');
const tests = [
['User Agent', results.userAgent],
['WebDriver', results.webDriver],
['WebDriver Advanced', results.webDriverAdvanced],
['Chrome', results.chrome],
['Plugins', results.plugins],
['Languages', results.languages],
['WebGL Vendor', results.webglVendor],
['WebGL Renderer', results.webglRenderer]
];
let allPassed = true;
for (const [name, result] of tests) {
const status = result.passed ? 'PASS' : 'FAIL';
if (!result.passed) allPassed = false;
console.log(`[${status}] ${name.padEnd(20)} ${result.value.substring(0, 50)}`);
}
console.log('\n' + '='.repeat(45));
if (allPassed) {
console.log('ALL TESTS PASSED - Bot detection bypassed!');
} else {
console.log('SOME TESTS FAILED - May be detected as bot');
}
console.log('='.repeat(45) + '\n');
const outOk = path.join(os.tmpdir(), 'bot-detection-result.png');
await page.screenshot({ path: outOk });
console.log(`Screenshot saved: ${outOk}\n`);
} catch (err) {
console.error('Error:', err.message);
await page.screenshot({ path: path.join(os.tmpdir(), 'bot-detection-error.png') }).catch(() => {});
} finally {
await browser.close();
}
}
testBotDetection().catch(console.error);
#!/usr/bin/env node
/**
* Stealth Browser Template v2.2
* Reusable factory for bot-detection-resistant browser automation.
*
* Design principle: with `channel: 'chrome'` + headed mode (both mandated here)
* real Chrome already supplies a genuine User-Agent, WebGL/GPU renderer, canvas
* fingerprint, PluginArray, and self-consistent permission/hardware values.
* rebrowser-playwright additionally hides the CDP `Runtime.enable` headless tell.
* So this template adds ONLY two things on top of that real environment:
* 1. strips Playwright's main-world artifacts (`window.__pwInitScripts` etc.),
* the deterministic signature detectors key on for `isPlaywright`;
* 2. enables rebrowser's Runtime-fix (env var, set below before import).
* It touches NOTHING on `navigator` — hand-rolled fakes (PluginArray, canvas
* noise, hardcoded hardwareConcurrency, a permissions override, a webdriver
* delete, a languages getter) were removed across v2.1/v2.2 because each created
* a detectable inconsistency (verified against bot-detector.rebrowser.net,
* deviceandbrowserinfo.com, browserscan.net — all green after the cleanup).
*
* MEASURED 2026-06-10 (macOS, headed Chrome): passes bot.sannysoft.com,
* bot-detector.rebrowser.net (0 red), deviceandbrowserinfo ("human"),
* browserscan ("Normal"). Does NOT defeat IP-reputation / behavioral / login
* walls, and `window.__playwright_builtins__` (a separate, non-configurable
* Playwright global) cannot be stripped — see SKILL.md "Detection Coverage".
*
* Usage:
* import { createStealthBrowser, humanDelay, humanType, simulateMouseMovement } from './stealth-template.mjs';
* const { browser, context, page } = await createStealthBrowser();
*
* Authorized-use only: respect each site's Terms of Service, robots.txt, and
* applicable law. Intended for QA, accessibility testing, and research.
*/
import { pathToFileURL } from 'node:url';
// rebrowser's Runtime.enable fix must be configured BEFORE the library is
// imported. Default it to 'addBinding' (keeps main-world access while hiding
// the CDP leak) unless the caller already set it. Then import dynamically.
process.env.REBROWSER_PATCHES_RUNTIME_FIX_MODE ??= 'addBinding';
const { chromium } = await import('rebrowser-playwright');
// Init script (runs at document_start in the page) that removes Playwright's
// main-world signature objects. Defined as a string-free function so Playwright
// serializes it for injection. Verified to flip isPlaywright true->false.
function stripPlaywrightArtifacts() {
const hide = (k) => {
try { delete window[k]; } catch { /* non-configurable */ }
if (Object.prototype.hasOwnProperty.call(window, k)) {
try { Object.defineProperty(window, k, { get: () => undefined, configurable: true }); } catch { /* sealed */ }
}
};
for (const k of Object.getOwnPropertyNames(window)) {
if (/^__pw|pwInitScripts|playwright/i.test(k)) hide(k);
}
// window.chrome presence for the rare headless fallback (no-op in real Chrome).
if (!window.chrome) window.chrome = {};
}
/**
* Create a stealth browser instance.
* @param {Object} options
* @param {boolean} options.headless - Run headed (default: false, required for stealth)
* @param {Object} options.viewport - Viewport size (default: { width: 1280, height: 800 })
* @param {string} options.userAgent - Custom user agent (optional; defaults to real Chrome UA)
* @param {string} options.locale - Browser locale (default: 'ko-KR'); sets navigator.languages + Accept-Language natively & consistently
* @param {string} options.storageState - Path to saved session state for cookie persistence (optional)
* @param {Object} options.proxy - Proxy config { server, username?, password? } (optional)
* @param {boolean} options.noSandbox - Add --no-sandbox (opt-in: needed for Linux root/CI, but is itself a bot signal — off by default)
* @returns {Promise<{browser, context, page}>}
*/
export async function createStealthBrowser(options = {}) {
const {
headless = false,
viewport = { width: 1280, height: 800 },
userAgent = null,
locale = 'ko-KR',
storageState = null,
proxy = null,
noSandbox = false
} = options;
const launchOptions = {
headless,
channel: 'chrome',
args: ['--disable-blink-features=AutomationControlled']
};
// --no-sandbox is a security risk AND an automation signal; opt in only when
// the environment requires it (e.g. running as root in Linux CI).
if (noSandbox) launchOptions.args.push('--no-sandbox');
if (proxy) launchOptions.proxy = proxy;
const browser = await chromium.launch(launchOptions);
// `locale` sets BOTH navigator.languages and the Accept-Language header
// natively and consistently (main thread + workers) — which is why we no
// longer override navigator.languages in JS (that created an own-property /
// worker-mismatch tell).
const contextOptions = { viewport, locale };
if (userAgent) contextOptions.userAgent = userAgent;
if (storageState) contextOptions.storageState = storageState;
const context = await browser.newContext(contextOptions);
// Strip Playwright's main-world artifacts on every navigation. This is the
// ONLY init script — it touches nothing on navigator.
await context.addInitScript(stripPlaywrightArtifacts);
const page = await context.newPage();
return { browser, context, page };
}
/**
* Save session state for cookie persistence.
* @param {BrowserContext} context
* @param {string} path - File path to save state
*/
export async function saveSession(context, path) {
await context.storageState({ path });
}
/**
* Add a human-like random delay between actions.
* @param {number} min - Minimum delay in ms
* @param {number} max - Maximum delay in ms
*/
export function humanDelay(min = 100, max = 500) {
return new Promise(resolve => {
const delay = Math.random() * (max - min) + min;
setTimeout(resolve, delay);
});
}
/**
* Type text with human-like speed.
* @param {Page} page - Playwright page
* @param {string} selector - Element selector
* @param {string} text - Text to type
*/
export async function humanType(page, selector, text) {
await page.click(selector);
for (const char of text) {
await page.keyboard.type(char);
await humanDelay(50, 150);
}
}
/**
* Simulate natural mouse movement on the page.
* Helps avoid Cloudflare Turnstile behavioral detection.
* @param {Page} page
* @param {number} moves - Number of movements (default: random 5-10)
*/
export async function simulateMouseMovement(page, moves) {
const count = moves ?? 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < count; i++) {
await page.mouse.move(
100 + Math.random() * 600,
100 + Math.random() * 400,
{ steps: 10 }
);
await humanDelay(50, 200);
}
}
// CLI: run this file directly to open a stealth browser at bot.sannysoft.com.
// pathToFileURL handles spaces / non-ASCII / Windows paths correctly.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
console.log('Testing stealth browser...');
const { browser, page } = await createStealthBrowser();
process.on('SIGINT', async () => {
await browser.close();
process.exit(0);
});
await page.goto('https://bot.sannysoft.com');
console.log('Browser opened. Check results in the browser window.');
console.log('Press Ctrl+C to close.');
}
Related skills
How it compares
Use playwright-bot-bypass when vanilla Playwright launches fail bot checks; prefer official site APIs when available and permitted.
FAQ
What packages does playwright-bot-bypass require?
playwright-bot-bypass expects rebrowser-playwright and the standard playwright package. Install with npm install rebrowser-playwright playwright, then npx playwright install chromium before running ab-test.mjs.
How does playwright-bot-bypass compare stealth vs normal mode?
playwright-bot-bypass runs ab-test.mjs to A/B test detection between standard Playwright Chromium and createStealthBrowser from stealth-template.mjs, logging which session sites flag as automated.
Is Playwright Bot Bypass safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.