Cua Docs

Drive a Web Page

Bind an exact browser window, inspect its active tab, and navigate, click, or type without foregrounding it.

Drive a Web Page

Use the browser tools when an agent must act inside Chromium or an Electron page without raising the native window. The tools bind the (pid, window_id) you selected to a DevTools target, then issue page input through that exact binding.

Chrome remains behind the agent terminal

Claude Code starts a YouTube video in Chrome without raising the browser window.

View the original post on X

Inspect the selected browser first#

First use list_apps and list_windows as usual, then call get_browser_state with the selected native (pid, window_id). If that browser already exposes an owned loopback DevTools endpoint, the driver binds it without changing the browser.

Do not pass remote-debugging flags or a user-data directory through launch_app. Cua Driver rejects those flags because they could expose a person's normal profile to DevTools.

Prepare an isolated browser when required#

If inspection returns browser_requires_setup, call browser_prepare as a separate approved operation. It starts another Chromium process with a driver-owned profile; it never copies, modifies, restarts, or terminates the selected user profile.

In an MCP client, approve the destructive tool call through the client's normal approval UI:

browser_prepare({
  "pid": 844,
  "session": "research-1",
  "allow_launch": true,
  "profile": {"mode": "isolated_new"}
})

The response includes prepared_pid. Call list_windows for that new process, then bind its window with get_browser_state. An isolated_new profile is removed when its owning session ends. Use profile: {"mode":"isolated_named","name":"research"} only when the automation profile must survive across sessions.

For direct CLI or raw-socket use, mint a five-minute, single-use token from an interactive terminal first:

cua-driver browser-approve --pid 844 --profile-mode isolated_new

Pass the returned token as approval_token in the otherwise identical browser_prepare call. get_browser_state never performs this setup itself.

Attach to an existing Chrome or Edge profile#

Use this route only when the agent must work in a supported Chromium profile that is already running and authenticated. Cua Driver does not restart the browser, copy the profile, or modify profile files. The route supports Chrome and Edge on macOS and Windows, plus Chrome in the validated Linux X11 and Sway configurations. Chromium and Edge have descriptor-backed Linux routes but are not yet product-validated there. Start a named driver session and mint an approval artifact for the exact process, native window, and session:

cua-driver browser-approve \
  --strategy existing_profile \
  --pid 844 \
  --window-id 10725 \
  --session research-1

The command requires an interactive terminal and asks you to type APPROVE. Pass its five-minute, single-use token to browser_prepare:

browser_prepare({
  "pid": 844,
  "window_id": 10725,
  "session": "research-1",
  "strategy": {"kind": "existing_profile"},
  "approval_token": "<token>"
})

The browser may display its own remote-debugging consent prompt. Cua Driver will press only the exact browser-owned semantic allow action for this approved process and window. An absent, ambiguous, dismissed, or unrecognized prompt is refused; it is never treated as generic permission to click security dialogs.

If the approved process has no DevTools endpoint, Cua Driver opens a temporary tab in the approved window and navigates to the product's fixed remote-debugging page (chrome://inspect/#remote-debugging or edge://inspect/#remote-debugging). It matches exactly one Allow remote debugging for this browser instance checkbox, verifies that it is off, and presses it once. It then proves that the new listener is loopback-only and owned by the approved process before closing the temporary tab. The result's side_effects reports whether the page was opened and closed, whether the address field was focused, whether the setting was enabled, and whether Chrome displayed a connection-consent prompt. If setup fails after a visible action, the structured refusal reports detail.setup_side_effects; a checkbox changed by that failed attempt is restored when its exact state can still be proven. Any ambiguous control or changed process/window identity is refused.

On Linux, launch the browser with --force-renderer-accessibility unless a screen reader already enables its complete AT-SPI tree. Native Wayland also requires a validated compositor route that can prove the exact process, window, geometry, and temporary focus restoration. A missing prerequisite is reported as a refusal; Cua Driver does not fall back to an unscoped desktop click.

After attached_existing_profile, list windows again if needed and call get_browser_state({pid, window_id, session}). Attachment invalidates older browser capabilities, so do not reuse a previous target_id, tab_id, or page ref.

The grant lives only in daemon memory. It expires after inactivity, has an absolute lifetime, and is revoked when its owning session ends or the daemon restarts. A dropped browser socket may reconnect up to three times under the same live grant; a successful reconnect invalidates capabilities again and requires another bind. Browser restart requires a new approval in this first release.

Existing-profile consent turns record redacted action metadata and results but omit screenshots and accessibility snapshots, so authenticated page content is not persisted as consent evidence.

Bind the native window#

Browser capabilities are scoped to a driver session. Start one and pass the same session value to every browser call:

start_session({"session": "research-1"})
 
get_browser_state({
  "pid": 844,
  "window_id": 10725,
  "session": "research-1"
})

Keep the returned opaque target_id. Use the tab_id whose active field is true only when selection is uniquely proven. active: false is proven unselected; active: null means the native window cannot distinguish tabs, such as two tabs with the same title. In that case, select an explicit tab by its returned metadata and never infer selection from list order. Mutation is available only when binding_quality is exact. A heuristic or ambiguous window match is refused.

Snapshot the tab#

Snapshot the selected tab before using an element ref:

get_browser_state({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "session": "research-1",
  "snapshot_format": "semantic_v2"
})

Read outline for visible page content. Select actions only from refs, and check that the chosen entry declares click or type in actions. content_refs are read capabilities for scope_ref; they cannot be passed to an unsupported mutation.

If snapshot.continuation is non-null, request the next ranked segment without changing the page:

get_browser_state({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "session": "research-1",
  "snapshot_format": "semantic_v2",
  "continuation": "<opaque_continuation>"
})

Use query to find matching roles, accessible names, or visible text. Use a current scope_ref to inspect one semantic subtree when names repeat in different regions. A continuation is single-use; a newer snapshot invalidates it.

Refs are valid only for that session, target, tab, document, frame, and latest snapshot. Take a fresh snapshot after navigation or whenever a call returns browser_ref_stale. dom_refs_v1 remains available for compatibility when snapshot_format is omitted, but new workflows should request semantic_v2.

Click and type#

The default click route requests Chromium's trusted input domain:

browser_click({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "ref": "p1:7",
  "session": "research-1"
})

Use viewport x and y instead of ref when the page snapshot cannot name the rendered target. Standalone Chrome and Edge on Windows have passing full-background evidence for this route. Standalone Chromium on macOS and Linux returns browser_input_trust_unavailable because dispatching the trusted pointer event activates the native browser window.

To request a synthetic full-background DOM click deliberately, pass input_route: "dom_event" with a ref:

browser_click({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "ref": "p1:7",
  "input_route": "dom_event",
  "session": "research-1"
})

Use this only when the page's DOM click semantics are acceptable. The driver never silently falls back from trusted input to a DOM event.

Snapshot again, then type into the fresh input ref:

browser_type({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "ref": "p2:3",
  "text": "status: ready",
  "session": "research-1"
})

The default insert_text mode is efficient for ordinary text. Use mode: "keystrokes" when the page depends on per-character keyboard events.

Use extended pointer actions#

browser_pointer supports hover, right_click, double_click, scroll, and drag. Use the trusted route when genuine browser input semantics are required and the platform reports that it can preserve posture. Use the explicit synthetic route with current refs when DOM event semantics are acceptable:

browser_pointer({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "ref": "p3:4",
  "action": "scroll",
  "input_route": "dom_event",
  "delta_y": 240,
  "session": "research-1"
})

For drag, pass destination_ref from the same exact frame. The driver refuses mixed-frame or stale destinations instead of translating them approximately.

Handle a page-owned dialog#

Prime and inspect the exact tab with browser_dialog({action:"inspect"}). When present is true, pass the returned dialog_id to accept or dismiss. Only an accepted prompt may include prompt_text. This tool does not handle browser permission prompts, extension UI, native sheets, or file pickers. Creating a Chromium native modal can activate its browser window; once you restore the intended occlusion, inspecting and resolving that exact dialog do not activate it again on Windows and macOS. Resolution defaults to delivery_mode:"background". Linux Chromium cannot resolve the native modal while preserving background posture, so the driver returns browser_input_trust_unavailable before dispatch. Retry with delivery_mode:"foreground" only when foreground activation is acceptable.

Assign files without a picker#

Take a semantic snapshot and choose a file-input ref whose actions contains upload. Call browser_set_input_files with absolute paths to direct regular files. Symlinks, directories, missing paths, and more than 32 files are refused. Verify the page's uploaded-file state afterward; the tool response contains only a count and never echoes the local paths.

Download into an approved directory#

Choose a current ref that activates the download and call browser_download through an MCP host that presents its destructive-tool approval. Pass an existing canonical absolute destination_root. The tool temporarily scopes Chromium's browser-wide download behavior, correlates the exact frame and opaque download id, restores the default on every outcome, and returns only the final byte count and opaque id. It does not return a filename, URL, or local path. A direct raw call without host approval is refused.

browser_navigate({
  "target_id": "<target_id>",
  "tab_id": "<tab_id>",
  "url": "https://example.com",
  "session": "research-1"
})

Navigation invalidates every prior ref for the tab. Wait for the destination, then call get_browser_state in snapshot mode again.

End the session#

end_session({"session": "research-1"})

Ending the session revokes its target, tab, and element capabilities.

Embedded webviews#

Electron has an exact route only while one proven native window maps to one CDP page. Adding another page or window invalidates that route. Tauri, WKWebView, WebKitGTK, and the split-process WebView2 shape currently return browser_route_unavailable unless the driver can independently prove the native-host-to-engine relationship. Continue with native get_window_state and AX/PX actions for those surfaces.

Legacy page actions#

The older page tool remains available for compatibility and for its read actions. It preserves its first-page and URL-hint behavior, but now shares the event-aware CDP transport used by the typed browser tools. New browser mutation workflows should use the typed browser tools so window correlation, endpoint ownership, capability lifetime, and refusal reasons remain explicit.