The playwright-stealth npm package hasn't been updated in over a year. If you're using it unchanged, you're leaving the most important detections unfixed.
Here are the 7 browser fingerprint patches that matter in 2026, with working code.
Why playwright-stealth Falls Short
The original playwright-stealth package patches ~12 JavaScript properties. Modern anti-bot systems check 40+. The most common failure modes in 2026:
- WebGL fingerprint not patched → GPU mismatch
-
navigator.permissionsnot patched → notification permission query returns wrong value -
window.chromenot complete → missingloadTimes()andcsi()methods - HTTP/2 fingerprint unchanged → TLS JA3 leaks Python/Node origin
- iframe detection not handled → nested iframes expose headless context
The 7 Patches
Patch 1: webdriver
await page.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
configurable: true
});
});
Note: undefined not false. Some detectors specifically check for false as a patched value.
Patch 2: plugins and mimeTypes
await page.addInitScript(() => {
const plugins = [
{ name: 'PDF Viewer', description: "'Portable Document Format', filename: 'internal-pdf-viewer' },"
{ name: 'Chrome PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
{ name: 'Chromium PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
{ name: 'Microsoft Edge PDF Viewer', description: "'', filename: 'internal-pdf-viewer' },"
{ name: 'WebKit built-in PDF', description: "'', filename: 'internal-pdf-viewer' }"
];
Object.defineProperty(navigator, 'plugins', {
get: () => Object.assign(plugins, { item: i => plugins[i], namedItem: n => plugins.find(p => p.name === n), refresh: () => {} }),
configurable: true
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => ({ length: 2, item: i => null, namedItem: n => null }),
configurable: true
});
});
Patch 3: window.chrome (the most commonly incomplete patch)
await page.addInitScript(() => {
if (!window.chrome) window.chrome = {};
window.chrome.app = {
isInstalled: false,
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' }
};
window.chrome.runtime = {
OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
id: undefined,
connect: () => {},
sendMessage: () => {}
};
// Required: loadTimes and csi (missing from most stealth libraries)
window.chrome.loadTimes = function() {
return {
requestTime: Date.now() / 1000,
startLoadTime: Date.now() / 1000,
commitLoadTime: Date.now() / 1000,
finishDocumentLoadTime: 0,
finishLoadTime: 0,
firstPaintTime: 0,
firstPaintAfterLoadTime: 0,
navigationType: 'Other',
wasFetchedViaSpdy: false,
wasNpnNegotiated: false,
npnNegotiatedProtocol: 'unknown',
wasAlternateProtocolAvailable: false,
connectionInfo: 'http/1.1'
};
};
window.chrome.csi = function() {
return {
startE: Date.now(),
onloadT: Date.now(),
pageT: 3000 + Math.random() * 1000,
tran: 15
};
};
});
Patch 4: navigator.permissions
await page.addInitScript(() => {
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: originalQuery(parameters)
);
});
Patch 5: WebGL vendor/renderer
await page.addInitScript(() => {
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.'; // UNMASKED_VENDOR_WEBGL
if (parameter === 37446) return 'Intel Iris OpenGL Engine'; // UNMASKED_RENDERER_WEBGL
return getParameter.call(this, parameter);
};
});
Replace with GPU values that match your target audience (Intel for consumer laptops, NVIDIA for power users).
Patch 6: languages and locale
await page.addInitScript(() => {
Object.defineProperty(navigator, 'language', { get: () => 'en-US' });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
});
Set to match your proxy location. German proxy + English language = mismatch signal.
Patch 7: iframe contentWindow isolation
await page.addInitScript(() => {
// Prevent iframe-based detection of patched properties
const origCreateElement = document.createElement.bind(document);
document.createElement = function(...args) {
const element = origCreateElement(...args);
if (args[0].toLowerCase() === 'iframe') {
Object.defineProperty(element, 'contentWindow', {
get: function() {
const win = this.contentWindow;
if (win) {
Object.defineProperty(win.navigator, 'webdriver', { get: () => undefined });
}
return win;
}
});
}
return element;
};
});
Combining Into a Utility Function
async function applyStealthPatches(page) {
await page.addInitScript(() => {
// Patch 1: webdriver
Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true });
// ... (all patches above)
});
}
// Usage
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
await applyStealthPatches(page);
When Patches Aren't Enough
For sites using Cloudflare Turnstile or enterprise-grade systems (DataDome, Kasada), even perfect browser patching isn't enough — the TLS fingerprint at the network level still exposes Node.js/Playwright.
At that point, the options are:
- Playwright + residential proxies + CAPTCHA services (~$3-8/1K requests)
- Pre-built cloud actors that handle the whole stack
For the most common scraping targets (LinkedIn, Amazon, Google Maps, Twitter), the Apify Scrapers Bundle ($29) includes actors pre-configured with stealth settings for each site — saving you the debugging cycle.
Which anti-bot system are you trying to work around? Drop a comment with the domain — I'll tell you whether patches are enough or if you need a different approach.
Top comments (0)