Back to Blog
Getting Started

Lightweight Browser Automation with Chrome DevTools Protocol

How to automate browsers using raw CDP commands without external frameworks, for lightweight and dependency-free browser scripting.

Introduction

Most browser automation relies on external frameworks like Playwright or Puppeteer. These tools are powerful but come with dependencies: Node.js, npm packages, framework-specific APIs, and runtime artifacts that exist in the browser environment. For lightweight tasks, monitoring scripts, or situations where minimizing your software footprint is a priority, the overhead is unnecessary.

BotBrowser offers an alternative with --bot-script. This flag loads a JavaScript file into a privileged browser context at startup, giving you direct access to the Chrome DevTools Protocol through the chrome.debugger API. No external runtime, no package installation, no framework artifacts in the page. Just a JavaScript file and the browser binary.

Why Bot Script Matters

The choice between a full automation framework and --bot-script is not about capability. Both can navigate pages, extract data, take screenshots, and interact with elements. The difference is in what they require and what they leave behind.

Frameworks install hundreds of megabytes of dependencies. They communicate with the browser over a WebSocket connection that adds latency and creates observable network traffic on localhost. They inject helper scripts and binding objects into the page context. For production automation where fingerprint consistency is critical, these artifacts are a concern.

Bot Script runs inside the browser itself. There is no external process communicating over WebSocket. There is no __playwright__binding__ or __puppeteer_evaluation_script__ in the page context. The script executes in an isolated privileged context that is not visible to page-level JavaScript. This makes it the cleanest option for automation where you want to minimize detectable signals.

Bot Script also simplifies deployment. Your automation is a single .js file alongside the BotBrowser binary and a profile. No node_modules, no package.json, no runtime version management. This is particularly valuable in containerized environments where image size matters.

Technical Background

The Privileged Context

When BotBrowser loads a script via --bot-script, it creates a special execution environment. This context has:

  • chrome.debugger API access: The same API available to Chrome DevTools extensions, allowing you to send any CDP command directly to browser targets.
  • Isolation from page context: Your script runs in its own world. Page JavaScript cannot see it, access its variables, or detect its presence.
  • Early execution: The script runs before the first page navigation completes, giving you the opportunity to set up interceptors and listeners before any page code fires.
  • Standard browser APIs: console, setTimeout, setInterval, fetch, XMLHttpRequest, and other browser APIs are available.

Chrome DevTools Protocol (CDP)

CDP is the low-level protocol that all browser automation tools ultimately use. Playwright, Puppeteer, and browser DevTools all send CDP commands over a WebSocket connection. Bot Script gives you direct access without the WebSocket layer.

CDP is organized into domains. Page handles navigation and lifecycle events. Network handles request interception. Runtime handles JavaScript evaluation in page contexts. DOM handles document structure. Each domain has methods you can call and events you can subscribe to.

How chrome.debugger Works

The chrome.debugger API follows a three-step pattern:

  1. Get targets: Call chrome.debugger.getTargets() to list available browser targets (pages, service workers, etc.).
  2. Attach: Call chrome.debugger.attach() to connect to a specific target. You must specify the CDP protocol version (typically "1.3").
  3. Send commands: Call chrome.debugger.sendCommand() to execute CDP methods on the attached target.

Events from the target are received through chrome.debugger.onEvent.addListener().

<svg viewBox="0 0 700 280" xmlns="http://www.w3.org/2000/svg" style={{maxWidth: '100%', height: 'auto'}}> bot-script.js Privileged Context chrome.debugger CDP Interface Browser Tab Page Context Execution Flow: 1. BotBrowser starts, loads profile 2. bot-script.js is injected into privileged context 3. Script calls chrome.debugger.getTargets() to find page 4. Script attaches to target and sends CDP commands 5. Page context never sees the script or its bindings

Common Approaches and Limitations

Playwright and Puppeteer

These frameworks provide high-level APIs for browser automation: page.click(), page.fill(), page.waitForSelector(), and similar methods. They handle the complexity of CDP communication, target management, and event synchronization. The trade-off is a large dependency tree, observable artifacts in the page context, and a Node.js runtime requirement.

For complex multi-step workflows with dozens of interactions, these frameworks save significant development time. For simpler tasks, they add unnecessary complexity.

Raw CDP over WebSocket

You can connect to Chrome's remote debugging port and send CDP commands over WebSocket using any language. This gives you full control but requires managing the WebSocket connection, handling protocol message serialization, and dealing with target lifecycle events manually. It also requires running an external process alongside the browser.

Selenium and WebDriver

Selenium uses the WebDriver protocol, which is a higher-level abstraction over CDP. It is widely used for testing but adds another layer of indirection and its own set of detectable signals. The WebDriver protocol itself sets navigator.webdriver to true in standard Chromium.

Bot Script Advantages

Bot Script combines the directness of raw CDP with the simplicity of a single-file deployment. No external process, no WebSocket management, no dependency installation. The script runs inside the browser with native access to the CDP interface.

BotBrowser's Approach

Launching with Bot Script

Pass the path to your JavaScript file using the --bot-script flag:

chromium-browser \
  --bot-profile="/opt/profiles/profile.enc" \
  --bot-script="/opt/scripts/automation.js" \
  --headless

The script loads immediately after the browser starts. You have full control from the earliest possible moment.

Basic Navigation Example

// automation.js - Navigate to a page and extract its title
chrome.debugger.getTargets(function (targets) {
  const page = targets.find(function (t) { return t.type === 'page'; });
  if (!page) {
    console.log('No page target found');
    return;
  }

  chrome.debugger.attach({ targetId: page.id }, '1.3', function () {
    if (chrome.runtime.lastError) {
      console.log('Attach failed:', chrome.runtime.lastError.message);
      return;
    }

    // Enable page events
    chrome.debugger.sendCommand(
      { targetId: page.id },
      'Page.enable',
      {},
      function () {
        // Navigate
        chrome.debugger.sendCommand(
          { targetId: page.id },
          'Page.navigate',
          { url: 'https://example.com' },
          function () {
            console.log('Navigation started');
          }
        );
      }
    );

    // Listen for page load
    chrome.debugger.onEvent.addListener(function (source, method, params) {
      if (source.targetId !== page.id) return;
      if (method === 'Page.loadEventFired') {
        // Evaluate JavaScript in the page context
        chrome.debugger.sendCommand(
          { targetId: page.id },
          'Runtime.evaluate',
          { expression: 'document.title' },
          function (result) {
            console.log('Page title:', result.result.value);
          }
        );
      }
    });
  });
});

Screenshot Capture

// Take a screenshot after page loads
chrome.debugger.sendCommand(
  { targetId: page.id },
  'Page.captureScreenshot',
  { format: 'png', quality: 100 },
  function (result) {
    // result.data contains base64-encoded PNG
    console.log('Screenshot captured, length:', result.data.length);
  }
);

Network Request Interception

// Enable network domain and listen for responses
chrome.debugger.sendCommand(
  { targetId: page.id },
  'Network.enable',
  {},
  function () {
    console.log('Network monitoring enabled');
  }
);

chrome.debugger.onEvent.addListener(function (source, method, params) {
  if (method === 'Network.responseReceived') {
    console.log(
      params.response.status,
      params.response.url.substring(0, 80)
    );
  }
});

Waiting for Elements

// Poll for an element to appear
function waitForSelector(targetId, selector, callback, maxAttempts) {
  var attempts = 0;
  function check() {
    attempts++;
    chrome.debugger.sendCommand(
      { targetId: targetId },
      'Runtime.evaluate',
      {
        expression:
          'document.querySelector("' + selector + '") !== null',
      },
      function (result) {
        if (result.result.value === true) {
          callback();
        } else if (attempts < (maxAttempts || 50)) {
          setTimeout(check, 200);
        } else {
          console.log('Timeout waiting for:', selector);
        }
      }
    );
  }
  check();
}

Configuration and Usage

Combined with Profile and Proxy

chromium-browser \
  --bot-profile="/opt/profiles/profile.enc" \
  --bot-script="/opt/scripts/scrape.js" \
  --proxy-server=socks5://user:pass@proxy.example.com:1080 \
  --bot-disable-debugger \
  --bot-disable-console-message \
  --headless

The --bot-disable-debugger flag prevents pages from pausing execution with debugger statements. --bot-disable-console-message suppresses console output from leaking through CDP.

When to Use Bot Script vs. Frameworks

ScenarioRecommended Approach
Single-page data extractionBot Script
Simple navigation and screenshotBot Script
Network request monitoringBot Script
Complex multi-page workflowsPlaywright / Puppeteer
Form filling with many fieldsPlaywright / Puppeteer
Parallel tab managementPlaywright / Puppeteer
CI/CD test suitesPlaywright / Puppeteer
Minimal container imageBot Script
No Node.js availableBot Script

Error Handling Pattern

function sendCommand(targetId, method, params, callback) {
  chrome.debugger.sendCommand(
    { targetId: targetId },
    method,
    params || {},
    function (result) {
      if (chrome.runtime.lastError) {
        console.log(
          'CDP error [' + method + ']:',
          chrome.runtime.lastError.message
        );
        return;
      }
      if (callback) callback(result);
    }
  );
}

Verification

Verify that your Bot Script is running correctly by checking the console output. BotBrowser logs script execution to standard output when using --bot-internal --v=1:

chromium-browser \
  --bot-profile="/opt/profiles/profile.enc" \
  --bot-script="/opt/scripts/automation.js" \
  --bot-internal --v=1 \
  --headless

You can also verify that no framework artifacts leak into the page context by evaluating common detection checks from within your script:

chrome.debugger.sendCommand(
  { targetId: page.id },
  'Runtime.evaluate',
  {
    expression: JSON.stringify({
      webdriver: 'navigator.webdriver',
      playwright: 'window.__playwright__binding__',
      puppeteer: 'window.__puppeteer_evaluation_script__',
    }),
  },
  function (result) {
    console.log('Artifact check:', result.result.value);
  }
);

Best Practices

Use absolute paths for --bot-script. Relative paths may resolve incorrectly depending on the working directory when the browser launches.

Handle errors at every callback level. CDP operations are asynchronous and any step can fail. Always check chrome.runtime.lastError before processing results.

Use Page.enable before listening for page events. CDP domains must be explicitly enabled before their events are dispatched.

Avoid long synchronous operations. The privileged context runs on the browser's main thread. Long-running synchronous code will block the UI.

Clean up attachments. If your script attaches to multiple targets, detach from them when done to avoid resource leaks.

Test incrementally. Start with a minimal script that just navigates and logs the title. Add complexity one step at a time.

Frequently Asked Questions

Can I use async/await in Bot Script?

The privileged context supports modern JavaScript features including async/await, Promises, and arrow functions. However, the chrome.debugger API uses callbacks. You can wrap it in a Promise for cleaner code:

function cdp(targetId, method, params) {
  return new Promise(function (resolve, reject) {
    chrome.debugger.sendCommand(
      { targetId: targetId },
      method,
      params || {},
      function (result) {
        if (chrome.runtime.lastError) {
          reject(new Error(chrome.runtime.lastError.message));
        } else {
          resolve(result);
        }
      }
    );
  });
}

Can I import external modules?

No. Bot Script runs in a browser context, not a Node.js environment. There is no require() or import for npm modules. All your code must be self-contained in the script file. You can concatenate multiple files into one before deployment if needed.

How do I save data to disk?

Bot Script does not have direct file system access. To save data, you can use fetch() to send data to an HTTP endpoint, or use CDP's Browser.grantPermissions and Page.downloadWillBegin for file downloads. For simple logging, console.log() output can be captured from the process's stdout.

Can I run multiple scripts?

The --bot-script flag accepts a single file path. To run multiple scripts, concatenate them into one file or have your main script dynamically load additional code via fetch() and eval().

How does Bot Script compare to browser extensions?

Bot Script runs in a context similar to a DevTools extension but without the extension manifest, permissions model, or content script injection. It has more direct access to CDP and does not require extension installation or management.

What CDP protocol version should I use?

Use "1.3" when calling chrome.debugger.attach(). This is the stable protocol version supported by all recent Chromium versions.

Can I interact with multiple tabs?

Yes. Call chrome.debugger.getTargets() to list all available targets, then attach to each one you want to control. You can also create new tabs using chrome.tabs.create() or the CDP Target.createTarget method.

What happens if the script crashes?

If your script throws an unhandled error, it is logged to console output but does not crash the browser. The browser continues running, and you can inspect the error in the logs when using --bot-internal --v=1.

Summary

Bot Script provides the lightest possible automation path with BotBrowser. A single JavaScript file gives you full CDP control without external frameworks, runtime dependencies, or detectable artifacts. It is ideal for simple tasks, monitoring scripts, and deployments where minimizing the software footprint is a priority.

For more complex workflows, see Getting Started with Playwright and Getting Started with Puppeteer. For production deployment patterns, see Docker Deployment Guide and Headless Server Setup.

#bot-script#automation#cdp#getting-started#framework-less