Introduction
When you automate a browser with Playwright, Puppeteer, or Selenium, the browser exposes signals that identify it as programmatically controlled. The navigator.webdriver property is set to true. Framework-specific objects appear in the JavaScript environment. Console and debugging protocols leave artifacts. Headless mode behaves differently from headed mode in subtle ways.
These signals exist because automation frameworks need hooks into the browser to function. The problem is that these same hooks are observable to any JavaScript running on the page.
The Observable Signals
navigator.webdriver
The W3C WebDriver specification introduced navigator.webdriver. When a browser is controlled via the WebDriver protocol, this property returns true. It was designed as a transparency mechanism, but it is now the most commonly checked automation signal.
In standard Chromium, this value is set at the C++ level during browser initialization when the --enable-automation flag is present, which Playwright and Puppeteer add by default.
Framework Injection Points
Each automation framework injects code at specific points:
- Playwright injects binding functions (
__playwright__binding__) and initialization scripts into every new page context - Puppeteer injects evaluation script helpers and CDP session management code
- Selenium uses the WebDriver protocol which sets browser-level automation flags
CDP Artifacts
The Chrome DevTools Protocol enables external tools to control the browser. When a CDP session is active, several signals become observable:
Runtime.enableandConsole.enablechange the behavior ofconsole.logand error stack traces- CDP session connections appear on specific WebSocket endpoints
- Certain browser behaviors change when CDP domains are active
Headless Mode Differences
Standard Chromium's headless mode differs from headed mode in several ways. The navigator.plugins array may be empty. WebGL may fall back to software rendering. Window dimensions may have unusual values.
Limitations of Stealth Plugins
Stealth plugins attempt to clean up after the framework has already set automation values. They override navigator.webdriver with a JavaScript getter, delete framework objects from the global scope, and patch other telltale properties.
This approach has a fundamental weakness: the cleanup happens in JavaScript, after the page context is initialized. The original values exist briefly before being overwritten, and the cleanup code itself is observable.
Sites can:
- Check for property descriptor modifications on
navigator.webdriver - Observe that
Object.getOwnPropertyDescriptorreturns a getter instead of a value - Notice timing differences between the initial state and the patched state
- Look for the plugin's own code patterns
How BotCloud Addresses This
BotCloud removes automation framework artifacts at the engine level, before any page code executes:
navigator.webdriverreturnsfalsenatively, not through a JavaScript override- No framework binding objects are present in page contexts
- CDP session activity is isolated from page JavaScript
- Headless and headed modes produce identical observable behavior
Because these changes happen in the browser engine itself, there is no JavaScript patch to inspect, no timing gap to query, and no property descriptor anomaly to find.
Practical Implications
With BotCloud, you can use Playwright or Puppeteer's full API surface without any stealth plugins or configuration workarounds. Your automation scripts stay clean and focused on business logic rather than fingerprint patching.
// No stealth plugin needed - just connect and automate
const browser = await puppeteer.connect({
browserWSEndpoint: 'wss://cloud.bots.win?token=YOUR_TOKEN',
});
const page = await browser.newPage();
await page.goto('https://example.com');
// navigator.webdriver is false, no automation traces leaked