How to Send Custom Chrome DevTools Protocol (CDP) Commands with Zendriver

Zendriver exposes the full Chrome DevTools Protocol through the cdp package, allowing you to send any CDP command by passing generator objects from domain modules like cdp.page or cdp.runtime to the Tab.send() method.

The cdpdriver/zendriver library provides a Python interface to control Chrome browser instances programmatically. When you need functionality beyond the high-level API, you can send custom Chrome DevTools Protocol (CDP) commands directly using the built-in cdp package generators and the Tab.send method.

Understanding the CDP Architecture in Zendriver

The Generator-Based Command Pattern

Zendriver represents every CDP domain (e.g., page, runtime, dom) as a Python module containing functions that return generator objects. According to the source code in zendriver/core/connection.py (lines 38-44), these generators are understood by the low-level Connection.send implementation and forwarded to the browser via WebSocket.

This design means that calling cdp.runtime.evaluate() does not execute immediately—it returns a generator describing the command parameters and target method. The actual transmission happens when you pass this generator to a sending mechanism.

The Four-Step Execution Flow

The typical flow for sending custom CDP commands involves these components:

  1. Create a Tab object – This holds a Connection that manages a WebSocket to the Chrome target.
  2. Call a CDP method – Invoking cdp.page.navigate(url='...') returns a generator describing the CDP command.
  3. Pass to Tab.sendTab.send forwards the generator to Connection.send (see implementation in zendriver/core/tab.py, lines 48-66).
  4. Handle the responseConnection.send (lines 34-71 in zendriver/core/connection.py) registers the request, ensures the necessary domain is enabled via Connection._register_handlers (lines 80-124), and awaits the decoded response or raises ProtocolException on error.

Enabling Domains and Executing CDP Commands

Many CDP domains require explicit enablement before use. The following example demonstrates enabling the Runtime domain and evaluating JavaScript directly using raw CDP commands, as shown in docs/tutorials/tutorial-code/cdp-1.py.

import asyncio
import zendriver as zd
from zendriver import cdp

async def main() -> None:
    browser = await zd.start()
    tab = await browser.get("https://example.com")

    # Enable the Runtime domain (required for many evaluate calls)

    await tab.send(cdp.runtime.enable())

    # Execute a custom CDP command – evaluate a script in the page context

    result = await tab.send(
        cdp.runtime.evaluate(
            expression="document.title",
            return_by_value=True,
            await_promise=False,
        )
    )
    print("Page title:", result[0].value)  # result is a list of RemoteObjects

    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

The cdp.runtime.enable() call returns a generator that activates the Runtime domain on the browser target. Subsequently, cdp.runtime.evaluate() constructs a generator describing the evaluation parameters, which tab.send transmits via the underlying Connection.send logic.

Sending Advanced Custom CDP Commands

Because Tab.send accepts any generator produced by the cdp package, you can issue commands not wrapped by high-level Zendriver helpers. This enables advanced scenarios such as custom network interception, performance tracing, or low-level DOM manipulation.

import asyncio
import zendriver as zd
from zendriver import cdp

async def main() -> None:
    browser = await zd.start()
    tab = await browser.get("https://example.com")

    # Directly send the Page.navigate command with a URL of your choice

    await tab.send(cdp.page.navigate(url="https://www.python.org"))

    # Wait for the navigation to finish

    await tab.wait_for_load_state("load")

    await browser.stop()

if __name__ == "__main__":
    asyncio.run(main())

Here, cdp.page.navigate() creates a generator for the Page.navigate command. By calling await tab.send(), you issue the navigation without relying on a dedicated high-level wrapper, while still benefiting from Zendriver's automatic target management and event handling.

Summary

  • Zendriver exposes all CDP domains through the cdp package, where each method returns a generator object describing the specific protocol command.
  • Use await tab.send() to execute any CDP command by passing generators from modules like cdp.page, cdp.runtime, or cdp.dom.
  • The underlying Connection.send method in zendriver/core/connection.py (lines 34-71) handles WebSocket transmission, automatic domain enablement, and response decoding.
  • This architecture allows you to mix high-level Zendriver APIs with low-level CDP commands for maximum flexibility in browser automation.

Frequently Asked Questions

What is the difference between Tab.send and Connection.send?

Tab.send provides the public API for sending CDP commands from a specific browser tab, implemented in zendriver/core/tab.py (lines 48-66). It forwards generators to Connection.send, which contains the core WebSocket logic in zendriver/core/connection.py (lines 38-44 and 34-71) that actually transmits the command to Chrome and manages the response lifecycle.

Do I need to enable CDP domains manually before sending commands?

Yes, many CDP domains require explicit enablement before use. The Connection._register_handlers method (lines 80-124 in zendriver/core/connection.py) manages domain state, but you should explicitly call enable methods like cdp.runtime.enable() or cdp.network.enable() before issuing commands that depend on those domains to ensure the browser target is ready to accept them.

Can I mix custom CDP commands with Zendriver's high-level API?

Absolutely. Since Tab.send works with any CDP generator while high-level methods use the same underlying infrastructure, you can interleave raw CDP commands with conveniences like tab.wait_for_load_state() or browser.get() within the same script without conflicts.

Where are the CDP domain modules defined?

The cdp package contains automatically generated Python modules for each CDP domain (e.g., zendriver/cdp/page.py, zendriver/cdp/runtime.py). These modules define functions that return generators describing the specific CDP protocol commands, their parameters, and return types according to the Chrome DevTools Protocol specification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →