How to Debug OpenWork Electron Issues Using Chrome DevTools Protocol (CDP)

OpenWork exposes a CDP endpoint at http://127.0.0.1:9825 by default, allowing you to connect Chrome DevTools or programmatic clients to inspect, interact with, and troubleshoot the Electron renderer process.

The OpenWork desktop application runs on Electron with built-in Chrome DevTools Protocol (CDP) support for development and debugging. Understanding how this CDP endpoint is created, how to connect to it, and how to use the helper utilities in the codebase enables efficient troubleshooting of startup failures, port conflicts, and UI behavior issues. This guide walks through the complete CDP debugging workflow as implemented in the different-ai/openwork repository.


How OpenWork Starts the CDP Endpoint

The CDP endpoint initialization flows through several key files:

Electron Main Process Launch

In apps/desktop/electron/main.mjs (around line 30), the Electron main process reads ELECTRON_EXTRA_LAUNCH_ARGS and starts Vite with --remote-debugging-port:

// apps/desktop/electron/main.mjs
// Launches Electron with CDP debugging enabled
const launchArgs = {
  // CDP port passed via ELECTRON_EXTRA_LAUNCH_ARGS
  remoteDebuggingPort: process.env.ELECTRON_EXTRA_LAUNCH_ARGS?.match(/--remote-debugging-port=(\d+)/)?.[1]
};

Surface Environment Configuration

In dev/evals/packages/hosts/src/local.ts (around line 350), the electronSurfaceEnv function generates an isolated profile folder and sets the environment variables:

// dev/evals/packages/hosts/src/local.ts
export function electronSurfaceEnv(opts: { cdpPort: number }) {
  const profileDir = mkdtempSync(path.join(tmpdir(), 'openwork-electron-'));
  
  return {
    ELECTRON_EXTRA_LAUNCH_ARGS: `--remote-debugging-port=${opts.cdpPort} --user-data-dir=${profileDir}`,
    OPENWORK_EVAL_CDP_URL: `http://127.0.0.1:${opts.cdpPort}`,
    OPENWORK_ELECTRON_LOG_PATH: path.join(profileDir, 'electron.log')
  };
}

Surface Handle Creation

In dev/evals/runner/surfaces.ts (around line 62), the electron() function creates a surface handle containing the CDP URL:

// dev/evals/runner/surfaces.ts
export async function electron(name: string, opts: ElectronSurfaceOpts): Promise<SurfaceHandle> {
  const cdpPort = await getFreePort();
  const env = electronSurfaceEnv({ cdpPort });
  
  return {
    kind: 'electron',
    name,
    cdpUrl: env.OPENWORK_EVAL_CDP_URL,  // Stored for debugging access
    meta: { log: env.OPENWORK_ELECTRON_LOG_PATH }
  };
}

CLI Output

When you run pnpm dev or pnpm dev:worktree, the CLI in dev/evals/runner/owt-cli.ts (around line 380) prints the CDP URL:


[openwork] electron main CDP: http://127.0.0.1:9825


Connecting Chrome DevTools to the Electron CDP Endpoint

Method 1: Chrome Inspect Page

  1. Open Chrome and navigate to chrome://inspect/#devices
  2. Click Configure... and add 127.0.0.1:9825
  3. Under "Remote Target," find your Electron window
  4. Click inspect to open DevTools attached to the renderer process

Method 2: Direct DevTools URL

Navigate directly to:


chrome-devtools://devtools/bundled/inspector.html?ws=127.0.0.1:9825/devtools/page/1

This provides full access to Elements, Console, Network, Application, and other DevTools panels.

Method 3: Programmatic Connection

Use the built-in CDP client from dev/evals/packages/cdp/src/cdp.ts:

import { waitForCdp, connect } from '@/dev/evals/packages/cdp/src/cdp';

const CDP_URL = 'http://127.0.0.1:9825';

// Verify endpoint is reachable
await waitForCdp(CDP_URL);

// Connect and inspect the page
const client = await connect(CDP_URL);
const { result } = await client.send('Runtime.evaluate', {
  expression: 'document.title'
});
console.log(result.value);  // Prints the page title

Using OpenWork's CDP Helper Library

The codebase provides high-level helpers in dev/evals/runner/context.ts for common debugging operations:

Available Helpers

Helper Purpose Location
browser_snapshot Captures accessibility tree and layout metrics context.ts
browser_click Sends synthetic click to element by UID context.ts
browser_fill Types into focused input elements context.ts
browser_eval Executes arbitrary JavaScript in renderer context.ts

Example: Taking a Snapshot and Clicking

import { browser_snapshot, browser_click } from '@/dev/evals/runner/context';

const CDP_URL = process.env.OPENWORK_EVAL_CDP_URL ?? 'http://127.0.0.1:9825';

// Capture current UI state
const snapshot = await browser_snapshot({ browser_url: CDP_URL });
console.log(`Found ${snapshot.nodes.length} accessibility nodes`);

// Find and click a button
const button = snapshot.nodes.find(n => n.role?.name === 'button');
if (button) {
  await browser_click({ 
    browser_url: CDP_URL, 
    uid: button.backendDOMNodeId 
  });
}

Note: All helpers throw descriptive errors if the CDP client cannot be obtained (see context.ts around line 195).


Common Debugging Scenarios

"CDP Endpoint Not Reachable"

Cause: Electron failed to start or port is in use.

Fix: Check the console banner for the actual CDP URL. Override with a free port:

export OPENWORK_EVAL_CDP_URL=http://127.0.0.1:9830
pnpm dev

Electron Logs Not Created

Cause: electronSurfaceEnv was not applied—profile isolation missing.

Fix: Always launch via pnpm dev or pnpm dev:worktree. The log path is stored in handle.meta.log from the surface handle.

CDP Command Timeouts

** Cause:** Default timeout of 20 seconds (DEFAULT_CDP_TIMEOUT_MS) exceeded.

Fix: Increase timeout in connection options:

import { connect } from '@/dev/evals/packages/cdp/src/cdp';

const client = await connect(CDP_URL, {
  connectTimeoutMs: 60000,
  sendTimeoutMs: 60000
});

Port Collision on Daytona

Cause: Daytona reserves ports 9825–9829 for Electron surfaces.

Fix: Use OPENWORK_EVAL_CDP_URL to point to a non-reserved port above 9829.

"Targeting a Specific CDP Target Requires cdpBaseUrl"

Cause: Helper called without base URL for page-specific operations.

Fix: Pass the base URL from the surface handle:

await browser_click({ 
  browser_url: handle.cdpUrl,  // Required parameter
  uid: targetId 
});

Complete Debugging Script

Save as scripts/debug-electron-cdp.ts and run with pnpm ts-node scripts/debug-electron-cdp.ts:

import { waitForCdp, connect } from '@/dev/evals/packages/cdp/src/cdp';
import { browser_snapshot, browser_click, browser_eval } from '@/dev/evals/runner/context';

const CDP_URL = process.env.OPENWORK_EVAL_CDP_URL?.trim() ?? 'http://127.0.0.1:9825';

async function main() {
  // 1. Verify CDP endpoint
  await waitForCdp(CDP_URL);
  console.log('✅ CDP endpoint reachable');

  // 2. Evaluate JavaScript directly
  const client = await connect(CDP_URL);
  const titleResult = await client.send('Runtime.evaluate', {
    expression: 'document.title'
  });
  console.log('📄 Page title:', titleResult.result.value);

  // 3. Use high-level helpers
  const snapshot = await browser_snapshot({ browser_url: CDP_URL });
  console.log(`🪞 Snapshot: ${snapshot.nodes.length} nodes`);

  // 4. Execute custom evaluation
  const customResult = await browser_eval({
    browser_url: CDP_URL,
    expression: 'window.location.href'
  });
  console.log('🔗 Current URL:', customResult.result.value);

  // 5. Interactive element detection
  const clickable = snapshot.nodes.find(n => 
    n.role?.name === 'button' || n.role?.name === 'link'
  );
  
  if (clickable) {
    console.log(`🖱 Clicking ${clickable.role.name}: ${clickable.name?.value}`);
    await browser_click({ 
      browser_url: CDP_URL, 
      uid: clickable.backendDOMNodeId 
    });
  } else {
    console.log('ℹ️ No clickable elements found');
  }
}

main().catch(err => {
  console.error('❌ Debug failed:', err);
  process.exit(1);
});

Key Source Files Reference

File Purpose
apps/desktop/electron/main.mjs Electron entry point with CDP port configuration
dev/evals/packages/hosts/src/local.ts Profile isolation and environment setup (electronSurfaceEnv)
dev/evals/runner/surfaces.ts Surface factory creating handles with CDP URLs
dev/evals/runner/context.ts High-level CDP helpers (browser_snapshot, browser_click, etc.)
dev/evals/packages/cdp/src/cdp.ts Low-level CDP connection, target discovery, and timeout handling
dev/evals/runner/owt-cli.ts CLI that outputs CDP URLs and log paths

Summary

  • OpenWork automatically exposes a CDP endpoint at http://127.0.0.1:9825 during development, controlled by ELECTRON_EXTRA_LAUNCH_ARGS in apps/desktop/electron/main.mjs
  • Connect via Chrome DevTools using chrome://inspect or programmatically using dev/evals/packages/cdp/src/cdp.ts
  • Use built-in helpers in dev/evals/runner/context.ts for snapshots, clicks, fills, and JavaScript evaluation
  • Override the default port with OPENWORK_EVAL_CDP_URL when facing conflicts
  • Check surface handles for log paths and CDP URLs when troubleshooting startup issues

Frequently Asked Questions

How do I find the current CDP URL for my OpenWork Electron instance?

Check the console output when running pnpm dev—the CLI prints electron <name> CDP: <url>. Alternatively, inspect the surface handle returned by electron() in dev/evals/runner/surfaces.ts, which stores the URL in handle.cdpUrl.

Can I use the CDP helpers outside the OpenWork codebase?

Yes, but you need to replicate the environment setup. The helpers depend on OPENWORK_EVAL_CDP_URL being set. Import the cdp.ts module and call connect() with any valid Chrome DevTools Protocol endpoint, not just OpenWork's Electron instance.

Why does my CDP connection timeout after 20 seconds?

The default timeout is defined in dev/evals/packages/cdp/src/cdp.ts as DEFAULT_CDP_TIMEOUT_MS (20000ms). Pass custom connectTimeoutMs or sendTimeoutMs values to connect() to extend this for slow operations or heavy pages.

How do I debug multiple Electron windows simultaneously?

Each surface created via electron() in dev/evals/runner/surfaces.ts receives a unique port via getFreePort(). Store each handle's cdpUrl separately and connect independent CDP clients to each endpoint. The listTargets() function in cdp.ts can enumerate pages within a single endpoint.

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 →