The Runtime.enable serialization tell
When an automation client sends Runtime.enable, Chrome starts forwarding console activity to the controller. To do that, it must serialize the arguments passed to console.log and friends so they can cross the protocol boundary. That serialization is observable: if you log an object that has a getter 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 see 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 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 ways:
- Console.enable - similar to Runtime; turning it on changes how console objects are handled and can be probed.
- Framework bindings - Playwright injects
window.__playwright__binding__/__pwInitScripts; older Puppeteer and ChromeDriver leave their own globals (cdc_properties). These are exposed-function artifacts of the CDP control channel. - Timing and event anomalies - CDP-dispatched input events can lack the trusted-event properties and natural timing of real user input.
- toString inspection - any JS patch a CDP driver injects to hide itself is itself inspectable.
The common thread is that the control channel and the scripts it injects exist in or affect page scope, and a sufficiently paranoid anti-bot script can find them.
Reducing the CDP surface
Mitigations, roughly in order of robustness:
- Do not enable
Runtime/Consoleunless needed, and delete framework bindings in an init script (delete window.__playwright__binding__; delete window.__pwInitScripts;). This removes the cheapest tells. - Hook earlier / lower - run automation logic in a privileged context before navigation rather than injecting into page 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 at the C++ level, so
Runtime.enableno longer changes page-observable behaviour.
The reason CDP detection matters so much is the same theme that runs through 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 specifically because the control channel is its own detection surface.
