
cdp
Drive Chrome via the DevTools Protocol from JavaScript. Run JS snippets through the browser-harness-js CLI — it auto-spawns a long-lived bun HTTP server holding a fully-typed CDP Session, and every call (browser-harness-js 'await session.Page.navigate(...)') executes against the same persistent connection. Session, active target, and globals survive across calls. Use when the user wants to automate, script, or inspect a Chrome browser via CDP — single tab or multi-tab, attach to existing Chrome or to a new one launched with --remote-debugging-port.
- Installs
- 706
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/browser-use/browser-harness-js --skill cdp- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads cdp automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
CDP — browser-harness-js skill
Custom codegen'd CDP SDK (every method from browser_protocol.json + js_protocol.json gets a typed wrapper) plus a tiny HTTP server that holds one persistent CDP Session. The browser-harness-js CLI auto-starts the server on first use and forwards JS snippets to it.
The SDK lives in the skill's sdk/ directory. In the rest of this doc, <skill-dir> refers to wherever npx skills add installed the skill (Claude Code: ~/.claude/skills/cdp; Cursor: ~/.cursor/skills/cdp; other agents vary). The CLI should be on PATH as browser-harness-js.
Setup (once, first use)
npx skills add drops the skill into your agent's skills directory but does NOT put the CLI on PATH. Before the first call, verify it's reachable and symlink it into any directory on your PATH if not:
# macOS (Apple Silicon + Homebrew)
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /opt/homebrew/bin/browser-harness-js
# macOS (Intel) / most Linux — may need sudo
command -v browser-harness-js >/dev/null || ln -sf <skill-dir>/sdk/browser-harness-js /usr/local/bin/browser-harness-js
# Linux without sudo (ensure ~/.local/bin is on PATH)
command -v browser-harness-js >/dev/null || { mkdir -p ~/.local/bin && ln -sf <skill-dir>/sdk/browser-harness-js ~/.local/bin/browser-harness-js; }
The CLI auto-installs bun on first run if it's missing (the server is Bun-native). Set BROWSER_HARNESS_SKIP_BUN_INSTALL=1 to opt out.
How to use
Just run browser-harness-js '<JS>'. The first call spawns the server in the background; subsequent calls hit the same process and so reuse the same session, the same WebSocket to Chrome, and any globals you set.
browser-harness-js 'await session.connect()'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
browser-harness-js '(await session.Runtime.evaluate({expression:"document.title",returnByValue:true})).result.value'
Output is the raw result content — no {ok,result} envelope.
| Result type | stdout |
|---|---|
| string | bare text, no JSON quotes (e.g. Example Domain) |
| number / boolean | 42, true |
| object / array (non-empty) | compact JSON (e.g. {"frameId":"..."}, [1,2,3]) |
undefined / null / "" / {} / [] | empty (no output) |
Errors go to stderr, exit code 1. The CDP error message and JS stack are printed verbatim, e.g.:
Error: CDP -32602: invalid params
at _call (.../session.ts:117:33)
...
Detect failure with if browser-harness-js '...'; then ...; else handle_error; fi or by checking $?.
Multi-line snippets via stdin (heredoc). Important: a multi-statement snippet does NOT auto-return the last expression — write return X explicitly. Single-expression snippets passed as the first argument DO auto-return.
browser-harness-js <<'EOF'
const tabs = await listPageTargets();
globalThis.tid = tabs[0].targetId;
await session.use(globalThis.tid);
return globalThis.tid;
EOF
CLI commands
| Command | Behavior |
|---|---|
browser-harness-js '<js>' | Auto-start server if needed, eval the JS, print result. |
browser-harness-js <<EOF…EOF | Same, code from stdin. |
browser-harness-js --status | Print health JSON (uptime, connected, sessionId) or exit 1 if down. |
browser-harness-js --start | Explicit start (no-op if already running). |
browser-harness-js --stop | Graceful shutdown. Drops session state. |
browser-harness-js --restart | Stop + start fresh. |
browser-harness-js --logs | tail -f the server log (/tmp/browser-harness-js.log). |
Env vars: CDP_REPL_PORT (default 9876), CDP_REPL_LOG (default /tmp/browser-harness-js.log).
API surface inside snippets
These globals are pre-loaded — no imports needed:
session— the persistentSession. Has every CDP domain mounted:session.Page,session.DOM,session.Runtime,session.Network, … 56 domains, 652 methods total.listPageTargets()— list real page targets via CDP'sTarget.getTargets(works on Chrome 144+ too), withchrome://anddevtools://URLs filtered out. No args — uses the connected session.detectBrowsers()— scan OS-specific profile dirs for running Chromium-based browsers with remote debugging on. Returns[{name, profileDir, port, wsPath, wsUrl, mtimeMs}], sorted by most recently launched.resolveWsUrl(opts)— resolve a WS URL from{wsUrl}|{port, host?}|{profileDir}. For the no-args auto-detect flow, callsession.connect()directly instead.CDP— the generated namespaces (CDP.Page,CDP.Runtime, …) for type-name reference.
Calling a CDP method
Every method takes a single object argument matching the CDP wire params; it resolves to the typed return value (no result envelope, no id correlation — handled for you).
// no params
await session.DOM.enable()
// required params
await session.Page.navigate({ url: 'https://example.com' })
// all-optional params (object also optional)
await session.Page.captureScreenshot()
await session.Page.captureScreenshot({ format: 'png', quality: 80 })
// returns are stripped to the typed shape
const { root } = await session.DOM.getDocument()
const { nodeId } = await session.DOM.querySelector({ nodeId: root.nodeId, selector: 'h1' })
Connecting
Default: just call session.connect() with no args. It auto-detects running Chromium-based browsers (Chrome, Chromium, Edge, Brave, Arc, Vivaldi, Opera, Comet, Canary) by scanning OS-specific profile dirs for a DevToolsActivePort file, ordered by most-recently-launched, and picks the first one whose WebSocket accepts. OS-agnostic — works on macOS, Linux, Windows.
await session.connect() // auto-detect
Use detectBrowsers() first if you want to see what's available (or let the user pick) before connecting:
const found = await detectBrowsers()
// [{ name: 'Google Chrome', profileDir, port, wsPath, wsUrl, mtimeMs }, ...]
Explicit forms — use these only when auto-detect picks the wrong browser, or when you already know where to connect:
| Form | When to use |
|---|---|
{ profileDir } | Target a specific browser when several are running. Reads <profileDir>/DevToolsActivePort directly. |
{ wsUrl } | You already have ws://…/devtools/browser/<uuid> (e.g. piped from elsewhere). |
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome' })
await session.connect({ wsUrl: 'ws://127.0.0.1:9222/devtools/browser/<uuid>' })
Profile paths by OS — use these with { profileDir }:
- macOS:
~/Library/Application Support/<Browser>(e.g.Google/Chrome,Comet,BraveSoftware/Brave-Browser,Arc/User Data) - Linux:
~/.config/<browser>(e.g.google-chrome,chromium,BraveSoftware/Brave-Browser) - Windows:
%LOCALAPPDATA%\<Browser>\User Data(e.g.Google\Chrome,Microsoft\Edge,BraveSoftware\Brave-Browser)
Per-candidate WS-open timeout defaults to 5s — live browsers answer with open/close within ~100ms, so 5s is already generous. The only case where 5s is too short is when Chrome is showing the Allow popup and waiting on the user to click. If you expect that, pass timeoutMs: 30000:
await session.connect({ profileDir: '/Users/<you>/Library/Application Support/Google/Chrome', timeoutMs: 30_000 })
If you see No detected browser accepted a connection — the browsers have DevToolsActivePort files but none are currently serving WS. Most common cause: remote-debugging is enabled but the user hasn't clicked Allow on the prompt yet. Tell them to click Allow, then retry (or bump timeoutMs).
Picking a target (tab)
After connect(), call session.use(targetId) once; subsequent page-level calls (Page/DOM/Runtime/Network/etc.) auto-route to that target's sessionId. Browser.* and Target.* calls always hit the browser endpoint.
const tabs = await listPageTargets() // no args; uses the connected session
const sid = await session.use(tabs[0].targetId)
await session.Page.enable()
await session.Page.navigate({ url: 'https://example.com' })
listPageTargets() uses CDP's Target.getTargets (not /json), so it works on Chrome 144+ too. It already filters out chrome:// and devtools:// URLs. Equivalent raw call:
const { targetInfos } = await session.Target.getTargets({})
const tabs = targetInfos.filter(t => t.type === 'page' && !t.url.startsWith('chrome://') && !t.url.startsWith('devtools://'))
To switch tabs: session.use(otherTargetId). To detach: session.setActiveSession(undefined).
Events
// Subscribe (returns an unsubscribe fn)
const off = session.onEvent((method, params, sessionId) => { ... })
// Or wait for a single matching event with optional predicate + timeout
await session.Network.enable()
const ev = await session.waitFor(
'Page.frameNavigated',
(p) => p.frame.url.includes('example.com'),
10_000
)
Persisting state across calls
Each snippet runs inside its own async wrapper, so its let/const declarations vanish when it returns. To carry data forward, attach to globalThis:
browser-harness-js '(await listPageTargets()).forEach((t,i)=>globalThis["tab"+i]=t.targetId)'
browser-harness-js 'await session.use(globalThis.tab0)'
browser-harness-js 'await session.Page.navigate({url:"https://example.com"})'
session itself, the active sessionId, and event subscribers are already preserved by the server — globals are only needed for ad-hoc data.
Connecting to a running Chrome (chrome://inspect flow)
When attaching to the user's already-running browser:
- Try
await session.connect()first (no args) — auto-detect handles every Chromium-based browser viaDevToolsActivePort. If it returns, you're done. - If auto-detect fails with
No running browser with remote debugging detected, the user needs to turn it on. Open the inspect page:Only macOS's AppleScript path avoids the profile picker; Linux/Windows may prompt the user to pick a profile first.# macOS — prefer AppleScript over `open -a` (reuses current profile, avoids the profile picker) osascript -e 'open location "chrome://inspect/#remote-debugging"' # Linux google-chrome 'chrome://inspect/#remote-debugging' # or: chromium, google-chrome-stable # Windows (PowerShell) Start-Process chrome 'chrome://inspect/#remote-debugging' - Tick "Discover network targets" in chrome://inspect, then click Allow when Chrome prompts.
- If auto-detect picks the wrong browser (multiple running, you want a specific one): list them with
await detectBrowsers(), thenawait session.connect({ profileDir: <the one you want> }). - If
session.connect()returnsNo detected browser accepted a connection, the user has remote-debugging on but hasn't clicked Allow yet. Tell them to click it and retry, or passtimeoutMs: 30000to wait for the click.
Working with targets (tabs)
- Filter Chrome internals.
listPageTargets()already dropschrome://anddevtools://URLs. If you callTarget.getTargets()directly, filter manually. - CDP target order ≠ visible tab-strip order. When the user says "the first tab I can see", use a screenshot or page title to identify it —
Target.activateTargetonly switches to a known targetId.
Looking up a method
The full typed surface is in <skill-dir>/sdk/generated.ts (~655 KB, only loaded if you read it). Each method has its CDP description as a JSDoc comment plus typed *Params / *Return interfaces in per-domain namespaces.
grep -n "navigate" <skill-dir>/sdk/generated.ts | head
Regenerating the SDK
When the upstream protocol JSONs change, replace sdk/browser_protocol.json and/or sdk/js_protocol.json and re-run:
cd <skill-dir>/sdk && bun gen.ts
browser-harness-js --restart # pick up the new bindings
Files
All paths are relative to <skill-dir> (the install path — see top of this doc).
/usr/local/bin/browser-harness-js→<skill-dir>/sdk/browser-harness-js(the CLI)sdk/repl.ts— HTTP server (Bun.serveon127.0.0.1:9876)sdk/session.ts—Sessionclass (transport, connect, target routing, events)sdk/generated.ts— codegen output: every CDP method as a typed wrappersdk/gen.ts— codegen scriptsdk/{browser,js}_protocol.json— upstream protocol (vendored)
FAQ
What does the cdp skill do?
Drive Chrome via the DevTools Protocol from JavaScript. Run JS snippets through the browser-harness-js CLI — it auto-spawns a long-lived bun HTTP server holding a fully-typed CDP Session, and every call (browser-harness-js 'await session.Page.navigate(...)') executes against the same persistent connection. Session, active target, and globals survive across calls. Use when the user wants to automate, script, or inspect a Chrome browser via CDP — single tab or multi-tab, attach to existing Chrome or to a new one launched with --remote-debugging-port.
How do I install cdp?
Run npx skills add https://github.com/browser-use/browser-harness-js --skill cdp in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/cdp/ (personal) or .claude/skills/cdp/ (project).
Which AI agents can use cdp?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is cdp safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains cdp?
It is published by browser-use in browser-use/browser-harness-js, and it is an official skill from the maker of the technology it covers. It has 706 recorded installs.
Popular productivity & automation skills
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.
lark-doc
open.feishu.cn
飞书云文档(Docx / Wiki)内容操作:读取、创建、编辑文档,插入或下载图片附件,以及操作思维笔记。用户提供文档 URL/token(包括 doubao.com 的 /docx/、/wiki/)时使用;按 URL 路径/token 而非域名路由;/page/ 是妙搭应用,非飞书云文档。文档内嵌资源按读取参考中的统一规则分流。独立评论操作走 lark-drive;随正文读取评论使用 docs +fetch。表格或 Base 内部数据操作不在本 skill。
lark-base
open.feishu.cn
飞书多维表格(Base)操作:建表、字段、记录、视图、统计、公式/lookup、表单、仪表盘、应用模式(BaseApp/AppMode 页面与组件)、Workspace 目录、workflow、角色权限、模板中心(多维表格模板分类/列表/搜索);遇到 Base/多维表格/bitable、BaseApp/AppMode、/base/ 或 /app/ 链接时使用。BaseApp 不走 lark-apps;文件导入/导出转 lark-drive,认证/授权转 lark-shared。
lark-drive
open.feishu.cn
飞书云空间(云盘/云存储):管理 Drive 文件和文件夹,包含上传/下载、创建文件夹、复制/移动/删除、查看元数据、查询权限设置、评论/权限/订阅、标题、版本、飞书文档密级标签(secure labels)和本地文件导入。用户需要整理云盘目录、处理云空间资源 URL/token、判断链接类型/真实 token/标题,或导入 Word/Markdown/Excel/CSV/PPTX/.base 为 docx/sheet/bitable/slides 时使用;doubao.com 云空间 URL/token 也按资源路径和 token 路由,不回退 WebFetch。不负责:文档内容编辑(走 lark-doc)、表格/Base 表内数据操作(走 lark-sheets/lark-base)、知识空间节点/成员管理(走 lark-wiki)、原生 Markdown 文件读写/patch/diff(走 lark-markdown)。
lark-shared
open.feishu.cn
Use for lark-cli setup/auth tasks: auth login/status/logout, user vs bot identity, business-domain permissions (--domain, including all/docs/drive), missing scopes, revoking authorization, or handling notice JSON.
lark-im
open.feishu.cn
飞书即时通讯:收发消息和管理群聊。发送和回复消息、搜索聊天记录、管理群聊成员、上传下载图片和文件、管理表情回复、发送应用内/短信/电话加急、发送和处理交互卡片(Interactive Card)、监听卡片按钮回调(card.action.trigger)。当用户需要发消息、查看或搜索聊天记录、下载聊天中的文件、查看群成员、搜索群、创建群聊或话题群、管理标记数据、管理 Feed 置顶(添加/移除/查询置顶会话)、管理标签数据、处理卡片回调时使用。