The Runtime.enable serialization tell
When an automation client sends the Runtime.enable command, Chrome starts forwarding console activity back to the controller. To do that, it has to serialize the arguments passed to console.log and similar calls — that is, convert them into data that can cross the protocol boundary (the gap between the page and the tool driving it). That conversion is observable: if you log an object that has a getter (a function that runs when a property is read) on one of its properties, the act of serializing the object invokes the getter — even though no human ever opened DevTools.
So the probe is tiny: create an object whose id (or toString, or a numeric coercion) is a getter, console.log it, and check whether the getter ran. On a normal browser nothing reads the property and the getter never fires. Under CDP with Runtime.enable active, the getter fires — revealing the automation channel. This is the same kind of Error.stack-style trap that catches frameworks which keep Runtime enabled by default.
Other CDP leaks
Beyond Runtime.enable, CDP-driven automation leaks in several other ways:
- Console.enable - similar to Runtime; turning it on changes how console objects are handled, which a page can probe for.
- Framework bindings - the tools leave their own fingerprints in the page. Playwright injects
window.__playwright__binding__/__pwInitScripts; older Puppeteer and ChromeDriver leave their own global variables (cdc_properties). These are leftover artifacts of the CDP control channel exposing functions to the page. - Timing and event anomalies - mouse and keyboard events dispatched through CDP can lack the trusted-event properties and natural timing of input from a real person.
- toString inspection - any JavaScript a CDP driver injects to hide itself can itself be inspected and exposed.
The common thread: the control channel, and the scripts it injects, live in or affect the page's own scope — so a sufficiently paranoid anti-bot script can find them.
Reducing the CDP surface
Ways to shrink these tells, roughly from easiest to most robust:
- Do not enable
Runtime/Consoleunless you actually need them, and delete the framework bindings in an init script that runs before the page loads (delete window.__playwright__binding__; delete window.__pwInitScripts;). This removes the cheapest tells. - Hook earlier / lower - run your automation logic in a privileged context before the page navigates, rather than injecting code into the page's scope, so there is less for the page to observe.
- Patch the engine - anti-detect browsers suppress the console serialization side effect and hide the bindings down in the C++ source, so
Runtime.enableno longer changes anything the page can see.
The reason CDP detection matters so much is the same theme that runs through all fingerprinting: a perfect static fingerprint does not help if the way you are driving the browser betrays you. Real-browser drivers and managed APIs that minimise the CDP footprint exist precisely because the control channel is its own detection surface.
