Nearly every browser automation stack that matters drives Chrome over the same channel: the Chrome DevTools Protocol. Puppeteer speaks it natively, Playwright speaks it to Chromium, and ChromeDriver has historically translated WebDriver commands into it. That shared dependency is a gift to defenders, because attaching a debugger to a browser changes the browser in ways page JavaScript can observe. An operator can randomize every fingerprint surface and still leak the one fact that matters, which is that something is instrumenting the session.

CDP detection sits inside the larger picture covered in headless browser detection; the per-framework artifacts live in the Puppeteer, Playwright, and Selenium articles.

What CDP is

The Chrome DevTools Protocol is the debugging interface Chromium exposes to its own DevTools. A client connects over a WebSocket and issues commands grouped into domains: Page for navigation, Network for traffic, Runtime for JavaScript evaluation, Input for synthetic events. DevTools uses it when a human opens the inspector. Automation libraries use it for everything.

The protocol was never designed to be invisible. It was designed for a developer debugging their own page, so its side effects were acceptable noise. Those side effects became the detection surface once the same protocol started driving bot traffic at scale.

The classic check: Runtime.enable

For years the standard CDP tell exploited a side effect of Runtime.enable, the command Puppeteer and Playwright issued on every new page to receive execution context events. With a debugger attached, serializing certain objects for the DevTools preview would touch properties that page JavaScript could instrument. The canonical version passed an Error object to console.debug with a getter planted on error.stack: if the getter fired without DevTools open, something was consuming console output over CDP (Rebrowser: Runtime.Enable CDP detection).

The check was cheap, ran in a few lines of inline JavaScript, and caught default Puppeteer and Playwright regardless of stealth plugins, because the stealth layers patched fingerprint surfaces rather than the protocol itself. Between roughly 2022 and 2025 it was the single highest-precision automation signal available to a page.

What changed in 2025

Two things ended the easy era.

Chrome changed the behavior. During 2025 a Chrome update altered the serialization path that the classic check observed, and the getter stopped firing on current versions. Detectors that keyed on it saw the signal quietly go dark, with no announcement, since from Chrome’s perspective it was an internal DevTools-preview detail and not a documented API.

The frameworks stopped calling Runtime.enable. A generation of CDP-minimal drivers emerged whose whole premise is avoiding the instrumented commands: nodriver (the successor to undetected-chromedriver), selenium-driverless, and rebrowser-patches for Puppeteer and Playwright. They skip Runtime.enable, work in isolated execution contexts created through Page.createIsolatedWorld, and defer debugger attachment until it is needed. The result is a session that is still fully automated but no longer trips the classic console-side-effect probes.

Anyone whose bot defense still keys on the 2023-era check is, as of mid-2026, testing for a population that has largely moved on.

What still works

The arms race did not end; it moved. The current detection surface has four parts.

Newer protocol side effects. The principle behind the classic check generalizes: anything that behaves differently when a debugger is attached is a viable probe. Variants observe other serialization paths, timing differences in console handling, and behaviors around debugger pauses. Each specific probe has a shelf life measured in months, since both Chrome and the evasion frameworks respond, but the category keeps producing signals because CDP cannot do its job without touching the runtime.

Injected residue. Frameworks leave objects behind. ChromeDriver’s $cdc_ property, Playwright’s __playwright__binding__ and init-script artifacts, and exposed-function wrappers from page.exposeFunction are all observable from page JavaScript. CDP-minimal drivers clean up the famous ones, which moves the work to enumeration: walking own-property lists on window, document, and prototype chains looking for names that no real browser session produces. The per-framework articles cover the current lists.

Evaluation fingerprints. Code injected through CDP executes differently from code that arrived with the page. Stack traces, the absence of a script URL, isolated-world boundary behavior, and the ordering of microtasks around injected evaluations are all measurable. An isolated world avoids main-world monkey-patches, but its DOM reads are still real DOM reads, and timing analysis on deliberately instrumented properties can expose the access patterns of an automation script that believes it is unobserved.

The absence pattern. A perfectly silent session is itself informative. Real Chrome run by a real user produces a constant low hum of observable activity. A session with a clean protocol surface, a server-class rendering environment, no behavioral channel, and a datacenter or rotating residential exit has answered the question through the joint distribution even though no single CDP probe fired. CDP evasion solves one layer out of five.

WebDriver BiDi changes the surface, not the problem

WebDriver BiDi is the W3C-standardized successor protocol, designed to work across Chromium, Firefox, and WebKit. It replaces WebDriver classic’s strict HTTP command-and-response model with a bidirectional WebSocket transport over which the browser streams events back to the automation client, which is the same capability that made CDP useful and proprietary in the first place (W3C WebDriver BiDi, still a Working Draft as of mid-2026 despite shipping in Chrome, Firefox, Selenium, and WebdriverIO). Selenium and Playwright have been migrating toward it, and Firefox automation already prefers it. For detection, BiDi replaces a well-mapped set of CDP artifacts with a newer, less-mapped set. The structural facts are unchanged: an automation protocol still attaches to the browser, still injects script, and still leaves residue, so the probe categories above carry over even as the specific checks get rewritten.

Detecting CDP in practice

A reasonable production ordering:

  1. Run the cheap residue checks. Known globals, known properties, exposed-binding artifacts. Catches default configurations instantly.
  2. Run the current side-effect probes. Whatever variants are live this quarter. High precision when they fire; treat silence as inconclusive rather than exonerating.
  3. Fold in the environment and behavior layers. GPU and font ground truth, pointer and typing presence, network coherence. This is what catches the CDP-minimal population.
  4. Cluster. A fleet of CDP-evading sessions still shares workload shape, timing cadence, and configuration residue across sessions.

The probes in steps 1 and 2 need a maintenance cadence. Treat them like signature content that expires, not like code you write once.

How Foil detects CDP

Foil’s SDK ships the current generation of protocol probes alongside the environment, behavioral, and network layers, and the sealed server-side scorer weighs them jointly. Protocol evidence is not exposed as public cdp.* signal strings; it surfaces in the verdict itself and in attribution.labels, for example a tool label with a value like puppeteer or playwright, so the application can distinguish “instrumented browser, high confidence” from “clean surface but no human present”:

import { safeVerifyFoilToken } from "@abxy/foil-server";

const result = safeVerifyFoilToken(sealedToken, process.env.FOIL_SECRET_KEY);
if (!result.ok) return next();

const { decision, attribution } = result.data;

if (decision.verdict === "bot") {
  // attribution.labels carries the tool when one is identified
  return res.status(403).json({ error: "automation_not_permitted" });
}

Because the probes rotate server-side, the operator cannot read the current checks out of the client bundle and patch against them. For the framing above this layer, see bot detection; for what to do with the verdict, bot mitigation.

Further reading