Reference

Command Reference

Custom comboboxes are scrolled into view instantly and opened with trusted pointer input. pick reports verified: false with a warning when the choice is unconfirmed. Page/container scrolling uses instant movement even with CSS smooth scrolling; an unchanged position returns scrolled: false with a warning.

This is the complete cheat sheet of every chrome-use command, alias, and flag. It's organized by command group so it's easy to scan and verify. To get a feel for the overall rhythm first, read the Core Loop; when a command doesn't work, check Troubleshooting.

ℹ️ Conventions
The @e1, @e2 in interaction commands are element references (refs) from snapshot output. Run snapshot -i first to get the refs, then act on them. Any command with a @ref always targets the tab that was active when the snapshot ran.

Valid incremental snapshots return JSON refs only for nodes in the delta. A no-change delta has empty refs; the complete live session map and previously captured refs remain available. A full-tree fallback keeps the normal full-snapshot behavior.

read --filter uses literal, ASCII-case-insensitive text matching, not a regex. It prefers matching page headings with their descendants, then matching sections. Use read --outline to find a heading. snapshot --filter separately accepts a regex. read --help lists the reading options.

Navigation

Open, navigate, go back/forward, and close the browser. Without a URL, open just launches the browser and stops at about:blank — handy for registering network interceptors, cookies, or init scripts before the first real navigation.

navigation
# launch the browser only, no navigation; stops at about:blank
$ chrome-use open
# launch and navigate (aliases: goto, navigate); prepends https:// when no protocol
$ chrome-use open <url>
$ chrome-use back                # go back
$ chrome-use forward             # go forward
$ chrome-use reload              # reload
# SPA client-side navigation: auto-detects Next.js router.push, falls back to history.pushState
$ chrome-use pushstate <url>
# Already on the target's origin and the page links to it? Click that link
# instead of navigating (react.dev: 19 requests in-page vs 47 for a navigation)
$ chrome-use open <url> --prefer-spa
$ chrome-use close               # close the browser (aliases: quit, exit)
$ chrome-use connect 9222        # connect over a CDP port
Blocked renderer recovery
With ab-connect 0.5.18 or newer, an explicit open or navigate whose renderer-scoped Page.navigate call times out is retried through Chrome's browser-level tab API. The command returns a warning and keeps the same session instead of restarting it.

You can line up all the pre-navigation setup in one shot with batch — launch cleanly, register interceptors / cookies in order, then navigate. This suits SSR-only debugging (block everything except --resource-type script), authenticating against a protected origin, or capturing a clean react suspense / vitals state.

pre-navigation batch
$ chrome-use batch \
    '["open"]' \
    '["network","route","*","--abort","--resource-type","script"]' \
    '["cookies","set","--curl","cookies.curl","--domain","localhost"]' \
    '["navigate","http://localhost:3000/target"]'

Snapshot (page analysis)

Interactive snapshots add a bounded context annotation for controls inside nearby articles, list items, rows, or groups identified by a heading or one distinct linked product name and one action control. Named product links omit repeated context when their sibling action carries it. It preserves local text such as product prices without changing accessible names or refs. Live status receipts also remain visible in interactive snapshots and action observations, with bounded text and explicit truncation. Context may be absent or marked truncated; use a scoped or full snapshot when a required detail is missing.

snapshot gives you a structured accessibility view of the page — the starting point for all interaction. snapshot -i is recommended: it lists only interactive elements, so the output is compact and every ref is present.

snapshot
$ chrome-use snapshot            # full accessibility tree
$ chrome-use snapshot -i         # interactive elements only (recommended)
$ chrome-use snapshot -c         # compact output
$ chrome-use snapshot -d 3       # limit depth to 3
$ chrome-use snapshot -i --dom   # actionable elements from a DOM walk (pierces shadow roots; automatic when the AX tree has no refs)
$ chrome-use snapshot -s "#main" # scope to a CSS selector
$ chrome-use snapshot -i -f "SSH|Save"   # keep matching lines + their ancestors (refs intact)
$ chrome-use snapshot -i --diff       # only what changed since the last snapshot
$ chrome-use snapshot -i --max-bytes 4000  # cut between nodes, report what was omitted + a cursor
$ chrome-use snapshot -i --max-bytes 4000 --from 70  # read on from where it stopped

Actions beyond a click

actions / do
$ chrome-use actions @e15   # what this element supports right now (read live)
$ chrome-use do @e15 expand  # perform one of exactly those; anything else is refused

expand / collapse, showMenu, increment / decrement and toggle, derived from the element's accessibility properties. The set is reported again afterwards, so a control that did not move cannot read as a success. See Interacting.

But only expand / collapse can be confirmed afterwards — the element must then offer the opposite action. For showMenu, toggle, increment and decrement the tree looks the same whether the control responded or ignored the click, so those are marked · rather than ✓ and carry confirmed: null in JSON — dispatched, outcome unknown. Read the page before treating one as done; never read null as success.

✅ Tip
snapshot -i only surfaces visible, interactive elements — it does not show hidden inputs or a control's actual submitted value. When a form "looks filled in" but won't pass validation, don't guess from the snapshot — inspect the DOM directly with eval.

Interactions

Drive the real browser with the @ref values from snapshot: click, type, check, drag, upload.

Drag mode follows the current session connection and target frame; an extension running in another Chrome profile does not change a launched browser’s drag mode.

interactions
$ chrome-use click @e1           # click
$ chrome-use click @e1 --new-tab # click and open in a new tab
$ chrome-use dblclick @e1        # double-click
$ chrome-use focus @e1           # focus the element
$ chrome-use fill @e2 "text"     # clear then type
$ chrome-use type @e2 "text"     # type directly (no clear)
$ chrome-use press Enter         # press a key (alias: key); ⚠ warns when the page has no listener for it
$ chrome-use press Control+a     # key combo
$ chrome-use keydown Shift       # hold a key down
$ chrome-use keyup Shift         # release a key
$ chrome-use hover @e1           # hover
$ chrome-use check @e1           # check a checkbox
$ chrome-use uncheck @e1         # uncheck
$ chrome-use select @e1 "value"   # pick a dropdown option
$ chrome-use select @e1 "a" "b"   # multi-select
$ chrome-use scroll down 500     # scroll the page (default down 300px)
$ chrome-use scroll down --until "#footer"  # step until the target (selector/@ref/text=) is in view
$ chrome-use scrollintoview @e1  # scroll element into the viewport (alias: scrollinto)
$ chrome-use drag @e1 @e2        # drag and drop
$ chrome-use upload @e1 file.pdf # upload a file
$ chrome-use download @e2 ./file.pdf # download from a link or control
$ chrome-use download-url https://example.com/file.pdf ./file.pdf
$ chrome-use downloads --limit 10 --json # machine-readable Chrome downloads

fill reads the resulting control or rich-editor model value back and compares it exactly before reporting success. If Monaco hides its model API, one trusted editor paste is verified through the editor's copy handler and the browser clipboard is restored afterward. An unverifiable paste or mismatched readback is an error, not a silent success. The error quotes both values (e.g. read back "" after writing "千代田区"); when every non-ASCII character vanished while ASCII survived it says the page filtered non-Latin input (a Latin-only / masked field), so re-typing will not help. type reads the field back too: if it does not contain the typed text, a ⚠ warning is printed (exit 0, JSON gains readBack). Text fields are written with trusted input events; fill warns when the form's Save / Submit is disabled before and after the fill, and click refuses a disabled control (#358).

press probes, for keys whose only effect depends on page JavaScript (Arrow/Home/End/PageUp/PageDown on a text field, Escape, Enter on a bare input outside a form), whether any keydown/keyup/keypress listener exists from the focused element up to window. With none found it still exits 0 but prints a ⚠ warning that the page cannot react to the key and suggests clicking the option instead (JSON gains keyListeners). Chords, Tab, Backspace and printable keys are not probed.

⚠️ Click "succeeds but nothing happens"
Autocomplete / menu <li> items often close when the input loses focus. If click reports success but the page doesn't react, retry that click with AGENT_BROWSER_CLICK_MODE=dom — a plain <li> has no focusable target, so the input keeps focus and the option still gets selected. Note that a DOM-dispatched click (the relay's default for left clicks) now moves focus like a real click: the clicked element, its nearest focusable ancestor, or a label's control receives focus unless the handler already moved it, so click <input> then press Meta+a lands on that input rather than the previously focused field.

Reading information

Read text, HTML, attributes, values, bounding boxes, and computed styles from elements or the page.

get
$ chrome-use get text @e1        # element text
$ chrome-use get html @e1        # innerHTML
$ chrome-use get value @e1       # input value
$ chrome-use get attr @e1 href   # attribute
$ chrome-use get title           # page title
$ chrome-use get url             # current URL
$ chrome-use get cdp-url         # CDP WebSocket URL
$ chrome-use get count ".item"   # number of matching elements
$ chrome-use get box @e1         # bounding box
$ chrome-use get styles @e1      # computed styles (font, color, background, etc.)

Check state

is
$ chrome-use is visible @e1      # is it visible
$ chrome-use is enabled @e1      # is it enabled
$ chrome-use is checked @e1      # is it checked

Screenshots & PDF

screenshot / pdf
$ chrome-use screenshot          # save to a temp directory
$ chrome-use screenshot path.png # save to a specific path
$ chrome-use screenshot main el.png # one element: selector, then path
$ chrome-use screenshot el.png --selector main # or --selector/-s, in any order
$ chrome-use screenshot --full   # full-page screenshot
$ chrome-use pdf output.pdf      # save as PDF

Headless Chromium hides native scrollbars in screenshots for consistent image output. Pass --hide-scrollbars false at launch to keep the native scrollbars.

Recording

record
$ chrome-use record start ./demo.webm    # start recording
$ chrome-use click @e1                   # perform actions
$ chrome-use record stop                 # stop and save
$ chrome-use record restart ./take2.webm # stop the current one + start a new recording

Waiting

Wait for an element, text, URL, network idle, or any JS condition before asserting state.

wait
$ chrome-use wait @e1                     # wait for an element
$ chrome-use wait 2000                    # wait a number of milliseconds
$ chrome-use wait --text "Success"        # wait for text (or -t)
$ chrome-use wait --url "**/dashboard"    # wait for a URL pattern (or -u)
$ chrome-use wait --load networkidle       # wait for network idle (or -l)
$ chrome-use wait --fn "window.ready"     # wait for a JS condition (or -f)

Mouse control

mouse
$ chrome-use mouse move 100 200      # move the mouse
$ chrome-use mouse down left         # press a button
$ chrome-use mouse up left           # release a button
$ chrome-use mouse wheel 100         # wheel

Semantic locators

An alternative to refs: locate by role, text, label, placeholder, alt, title, testid, or position, and chain the action to perform directly after it. Or pass a natural-language description to return ranked candidates without acting.

find
$ chrome-use find role button click --name "Submit"
$ chrome-use find "edit web service settings button"  # candidates only
$ chrome-use find text "Sign In" click
$ chrome-use find text "Sign In" click --exact      # exact match only
$ chrome-use find label "Email" fill "user@test.com"
$ chrome-use find placeholder "Search" type "query"
$ chrome-use find alt "Logo" click
$ chrome-use find title "Close" click
$ chrome-use find testid "submit-btn" click
$ chrome-use find first ".item" click
$ chrome-use find last ".item" click
$ chrome-use find nth 2 "a" hover

Browser settings

set
$ chrome-use set viewport 1920 1080          # viewport size
$ chrome-use set viewport 1920 1080 2        # 2x retina (CSS size unchanged, higher-res screenshots)
$ chrome-use set device "iPhone 14"          # emulate a device
$ chrome-use set geo 37.7749 -122.4194       # geolocation (alias: geolocation)
$ chrome-use set offline on                  # toggle offline mode
$ chrome-use set headers '{"X-Key":"v"}'     # extra HTTP headers
$ chrome-use set credentials user pass       # HTTP basic auth (alias: auth)
$ chrome-use set media dark                  # emulate color scheme
$ chrome-use set media light reduced-motion  # light + reduced motion

Cookies & storage

cookies / storage
$ chrome-use cookies                     # get all cookies
$ chrome-use cookies set name value      # set a cookie
$ chrome-use cookies clear --domain example.com  # clear one site's cookies; --all --yes clears the whole browser
$ chrome-use storage local               # all of localStorage
$ chrome-use storage local key           # a specific key
$ chrome-use storage local set k v       # set a value
$ chrome-use storage local clear         # clear everything

Import cookies from cURL

Auto-detects the format: a JSON array of {name, value}, a DevTools → Network → Copy as cURL dump, or a bare Cookie header. Cookie values are never echoed back on error.

cookies set --curl
$ chrome-use cookies set --curl <file>                       # auto-detect JSON / cURL / Cookie header
$ chrome-use cookies set --curl <file> --domain example.com  # scope to a domain

Network interception

Intercept, block, mock responses, rewrite requests, or edit real responses. For in-depth usage, see the network section in the Core Loop.

network
$ chrome-use network route <url>              # intercept requests
$ chrome-use network route <url> --abort      # block requests
# mock a response (fulfill)
$ chrome-use network route <url> --body '{}' --status 200 --header K=V --content-type application/json
# rewrite a request (continue)
$ chrome-use network route <url> --method POST --set-body '{}' --set-header K=V --rewrite-url <u>
# edit a real response
$ chrome-use network route <url> --edit-status 503 --edit-header K=V --replace 'from=>to'
$ chrome-use network unroute [url]            # remove a route
$ chrome-use network requests                 # view tracked requests
$ chrome-use network requests --filter api    # filter requests

Intercept by resource type

network route --resource-type
# block scripts only (SSR-lock mode)
$ chrome-use network route '*' --abort --resource-type script
# replace images and fonts with empty responses
$ chrome-use network route '*' --resource-type image,font --body ''

Tabs & windows

Tab ids are stable strings like t1, t2 and are not reused within a session. After a reconnect (a relay restart, for example) each id stays bound to the same Chrome tab; an id whose tab cannot be found again is refused rather than given to another tab, and its label stays reserved for that tab. If the tab you were driving is gone, commands on the current tab are refused until you pick one with tab <id>, tab new <url> or --tab <id>. Positional integers are not accepted — tab 2 errors; use t2. You can also give a tab a readable label (docs, app), interchangeable with the id.

tab / window
$ chrome-use tab                              # list tabs (with tabId and label)
$ chrome-use tab new [url]                    # new tab
$ chrome-use tab new --label docs [url]       # new tab with a readable label
$ chrome-use tab new [url] --activate         # activate before initialization
$ chrome-use tab duplicate [ref]              # native Duplicate tab; current by default
$ chrome-use tab duplicate docs --label copy # duplicate by ref and label the copy
$ chrome-use tab t2                           # switch by id
$ chrome-use tab select t2                    # explicit switch syntax
$ chrome-use tab select t2 --activate         # activate before the liveness probe
$ chrome-use tab adopt "example.com/stuck" # adopt without navigating
$ chrome-use tab adopt "example.com" --activate # adopt and leave in the foreground
$ chrome-use tab inspect t2                   # browser state without page JavaScript
$ chrome-use tab docs                         # switch by label
$ chrome-use tab close                        # close the current tab
$ chrome-use tab close t2                     # close by id
$ chrome-use tab close docs                   # close by label
$ chrome-use tab list --all                   # every tab in the profile, the user's too (read-only)
$ chrome-use --tab chrome-tab:812 --force snapshot -i # act on the user's tab in place (user asked)
$ chrome-use tab close chrome-tab:812 --force # close the user's tab (user asked)
$ chrome-use window new                       # new window

tab list --all answers "which tabs do I have open?": every tab of the connected Chrome profile, across all windows, including the user's own and other chrome-use sessions'. It only observes (over the relay chrome.tabs.query and ABExt.state, ab-connect 0.5.25+; on direct CDP Target.getTargets and Browser.getWindowForTarget): even the first call starts no daemon and creates no window or tab. Nothing is attached, activated, focused, moved or reloaded, and no page content is read. A running session keeps its existing browser. A missing non-relay daemon needs --cdp; inventory refuses to launch a browser, provision a provider or upgrade a daemon. Action-policy restrictions still apply. Each row has a handle (chrome-tab:<id> over the relay, the targetId on direct CDP), its window, whether it is the active tab of that window, pinned/incognito/discarded (relay), title, url, and its owner: {kind: "self"}, {kind: "session", session, live} or {kind: "user"}, also in words (this session, session alpha (live), the user). This session's tabs come first, then other sessions' by name, then the user's, each by window and tab position. At most 200 rows (--limit, up to 1000); titles over 300 and urls over 2048 characters are cut, and the reply says what it left out. Urls are exactly as Chrome reports them and may carry session tokens.

Acting on a tab the session does not own needs --force, and only when the user asked for that tab. --tab <handle> --force <command> runs any page command on it in place: it is attached but not activated, focused or moved (to bring it forward too, tab adopt <handle> --activate). tab close <handle> --force closes it and reads it back. Each forced result has forced: true and forcedTab (title, url, owner); another live session's tab also gets a warning. Without --force the refusal names the owner and the flag. close never closes a forced tab: it stays open and is released (forcedTabsLeftOpen; over the relay this needs ab-connect 0.5.34, an older extension keeps it attached until it restarts). chrome-tab: handles need ab-connect 0.5.30+.

Page diagnostics: diag pages

$ chrome-use diag pages --json                       # every page, owner, stale relay records, extension version (attaches nothing)
$ chrome-use diag pages --measure --json             # + heap / nodes / listeners of pages already attached
$ chrome-use diag pages --watch 60 --force --json    # two samples 60 s apart, a leak class per page

diag pages is the stable, read-only, versioned report (JSON "schema": 1) for tools that look for leaky pages, such as Thermo, instead of driving the relay with raw CDP. It runs on its own diagnostic connection: no daemon, no session tab, no relay restart. Per page: handle, targetId, url, title, windowId, active / visible / discarded (relay), attached and the owner (self / session / user). On top: the count of stale relay records, the extension version and whether it is behind the published one (with the update / reload hint doctor gives), the profile and the relay connection state.

Without --measure it never attaches anything. --measure adds JSHeapUsedSize, Nodes, JSEventListeners and Documents (Performance.getMetrics) for this session's pages and pages chrome-use already holds attached that no other running session holds, and workers per site (Runtime.getHeapUsage); every other page is skipped with a reason. --force (with --measure / --watch) measures the rest: a tab that is not attached is attached, measured and released at once (ab-connect 0.5.34+), never activated or focused. Chrome shows its debugging bar on it meanwhile, so use it only when the user asked. --watch <s> takes two samples s seconds apart and adds per-page deltas and a leak class: listeners_growing_nodes_flat, heap_growing (≥ 1.5 MiB/min), nodes_climbing or none. Output is bounded (--limit pages, default 200, max 1000; titles cut at 300 and urls at 2048 characters) and says what it left out. Exit codes: 0 ok, 1 usage, 2 the extension relay (or the --cdp endpoint) is not connected, 3 --measure without --force when every page is held by another running chrome-use session. The JSON shape is documented in the core skill's references/diag-pages.md.

tab close on the session's only tab, when the session created it, ends the session the way close does (sessionClosed: true), so no separate close is needed. A tab the session did not create (adopted, or the user's) is never closed; the refusal says to leave it open and run close.

close reads every tab it closed back before reporting it: over the relay from the extension's ABExt.tabPresence (ab-connect 0.5.33+: Chrome's target registry for that exact target, then the exact tab id; any API error counts as unconfirmed), or from Chrome's target list on a direct CDP connection. An older extension cannot confirm a tab is gone, so its tabs stay unverified. The whole close has one 15 s budget; a tab not confirmed by then stays unverified and keeps its ownership. Closing one of several tabs (tab close t2) is read back too: a tab not confirmed gone fails and keeps its close right. It returns tabsClosed (the tabs confirmed gone) and verifiedAbsent: true. If a tab the session created is still open, or cannot be confirmed gone (unverified), it fails with close incomplete and names those tabs; the session, its daemon and its tab ownership are kept, so retry close.

Native duplication requires real Chrome connected through the chrome-use extension. It restores the previously visible foreground tab, while the duplicate becomes chrome-use's internal active tab. There is no same-URL fallback for launched Chrome, raw CDP, Lightpanda, or cloud providers. Chrome decides whether the background duplicate starts loading.

tab select <ref> is equivalent to tab <ref>. tab adopt <URL substring|targetId> attaches an existing tab in the current session without reloading it, through either the extension or direct CDP. For direct CDP use chrome-use --session example --cdp 9222 tab adopt <targetId> and retain the same session and endpoint for subsequent commands; top-level adopt still selects the extension relay. On external or extension-connected Chrome, tab list marks tabs as created, adopted, or foreign. Only created/adopted tabs can be selected, and only tabs created by the current session can be closed; adopted tabs remain user-owned. Created ownership survives daemon restarts for the same named session and connected browser endpoint, so interrupted cleanup can resume safely. tab inspect <ref> reads Chrome's browser-level tab metadata, so it can still return URL, load status, discard/freeze state, and debugger attachment when page JavaScript blocks the renderer main thread. eval still cannot complete until that thread responds. On the extension relay, tab inspect requires ab-connect 0.5.16 or newer. A failed tab select liveness probe does not by itself prove that the renderer is stuck. If the warning reports a live extension older than the bundled version, update or reload it at chrome://extensions, then retry.

A tab that a plain click opens (target=_blank, window.open) is reported as openedTab and becomes one of the session's tabs: listed by tab list, closed by tab close and by close. --follow switches to it, and with --observe the settle and snapshot then describe the new tab; if the switch fails, followed is false and the session stays on the clicked tab. In your own Chrome only a tab opened by one of the session's tabs is taken (in the session's tab group, or ungrouped with a session tab as opener); a tab in another group, or one you open, is left alone. It is attached by Chrome tab id, which needs ab-connect 0.5.30 or newer. A tab seen but not taken sets openedTabWarning and openedTabStatus: unadopted (left alone on purpose) or unknown (Chrome or the extension did not answer within the check's budget of about 2 s to find plus 3 s to attach). tab adopt <url> picks it up later.

A tab a page opens itself (target=_blank, window.open) brings Chrome to the front over whatever app you are in (#468). By default chrome-use leaves it to Chrome, exactly like a user's click, and in your own Chrome the click says so in openedTabWarning. On macOS chrome-use then puts the app that was in front back in front (on by default): only when Chrome comes to the front within 1.5 s of the click with a tab that was not there before, never when Chrome was already in front or you moved to another app (focusRestored / focusNote on the click). Not done on Windows and Linux. AGENT_BROWSER_RESTORE_FOCUS=0 turns it off. Opt-in: AGENT_BROWSER_BACKGROUND_LINKS=cross-site (also 1/on) makes chrome-use open cross-site target=_blank links itself, in a tab the session created (left click, no modifiers, http(s) href to another registrable domain by the Public Suffix List on both ends, no rel=opener, download or ping): the page's click handlers all run first, and if none cancelled the click the link opens in a background tab of the session (openedTabMode: "background"), with the page as referrer (ab-connect 0.5.31+). Its cost against Chrome's own click: one extra about:blank history entry, and a link that redirects back to the page's own site arrives there without its SameSite=Strict cookies (the user can appear signed out). all also takes same-site links, which then always lose those cookies; unset, 0 or off never (the default). Even when on, these go through Chrome and the click says so: same-site links (unless all), IP addresses, localhost and hosts outside the Public Suffix List, links a handler changes to another host during the click, links a ChooseBrowser rule applies to, unclassifiable pages, window.open, rel=opener, named targets, forms, handlers that stop the click, and pop-ups opened later. The daemon reads the variable when it starts: set it before the session's first command, or close and start again.

tab new, tab select, and tab adopt stay in the background by default. Add --activate (alias --front) to raise the target before renderer initialization or the liveness probe and leave it in the foreground. This changes the visible tab and focuses its Chrome window over whatever the user is doing, so use it only when the user asks; activation alone does not prove that page reads work. If new-tab initialization fails, the error reports the retained target ID. Use tab select <targetId> --activate with the same session and connection endpoint, then snapshot -i to verify recovery. Do not repeatedly create tabs or automatically replay a click whose outcome is unknown.

ℹ️ refs belong to the tab active at snapshot time
The daemon maintains a single active tab, and @eN belongs to the tab that was active when the snapshot ran. To operate on another tab, switch to it first, then snapshot. Labels are never auto-generated, aren't rewritten after navigation, and are unique within a session.

Frames (iframe)

iframes are detected automatically during a snapshot, and their contents are inlined beneath the iframe element (one level of nesting). refs inside an iframe can be operated on directly; you can also switch frame context for a scoped snapshot.

frame
$ chrome-use frames              # [0] top … [1] accessory_layer …
$ chrome-use frame 1             # switch by the index frames prints (0 = top document)
$ chrome-use frame "#iframe"     # switch to an iframe by CSS selector
$ chrome-use frame @e3           # switch to an iframe by element ref
$ chrome-use frame main          # return to the main frame

A cross-origin overlay is a separate frame — a bank/branch picker, a payment field. snapshot -i on the page shows nothing of what is inside it, and focus / press land on the iframe element itself. The path is frames → frame <index> → snapshot -i / click / fill inside it; eval --frame <f> is the one-off form.

frame accepts three kinds of input: an element ref (frame @e3), a CSS selector (frame "#payment-iframe"), and a frame name / URL (matched against the browser's frame tree).

Dialogs

By default alert and beforeunload are accepted automatically so the agent doesn't block; confirm and prompt still need explicit handling. A click that opens either one returns a pending-dialog result immediately, so the same session can run the commands below. Use --no-auto-dialog to turn that behavior off.

dialog
$ chrome-use dialog accept [text]  # accept the dialog
$ chrome-use dialog dismiss        # dismiss the dialog
$ chrome-use dialog status         # check whether a dialog is open

Running JavaScript

eval runs in the page's MAIN world, so state persists and handlers fire. When your code contains nested quotes or special characters, -b / --stdin is more reliable — shell escaping is easy to get wrong.

eval
$ chrome-use eval "document.title"          # simple expressions only
$ chrome-use eval -b "<base64>"             # arbitrary JS (base64-encoded)
$ chrome-use eval --stdin                   # read the script from stdin

# multi-line scripts via stdin + heredoc:
$ cat <<'EOF' | chrome-use eval --stdin
const links = document.querySelectorAll('a');
Array.from(links).map(a => a.href);
EOF

Debug forms / hidden state with eval

form debugging
# dump every field's name → value, including hidden inputs and unchecked radios
$ chrome-use eval "JSON.stringify([...document.forms[0].elements].map(e=>({name:e.name,type:e.type,value:e.value,checked:e.checked})).filter(e=>e.name))"
# why won't it submit? ask the browser's own validity API
$ chrome-use eval "[...document.forms[0].elements].filter(e=>!e.validity?.valid).map(e=>e.name+': '+e.validationMessage)"

Single-pass scripting (batch / script)

Multi-step flows don't need one round-trip per command. batch runs a fixed sequence in one round-trip; script adds values between steps, loops, conditions, and asserts. See Single-pass Scripting.

Ordinary CLI --json replies use a success/data/timing envelope; --no-timing (or AGENT_BROWSER_TIMING=0) leaves timing out for a compact reply, and timing.jsonl is still written. batch --json prints an array of {command,success,result,error} entries; script --json prints the bare program result (ok, return, logs, error, advisories, and other program fields). Batch and script CLI output have no top-level timing.

A batch step takes its own per-command flags (--observe, --no-settle, --settle-ms, --with-screenshot, --if-present, --new-tab, --tab): batch "fill @e1 Ada" "click @e2 --observe" puts observed in that step's result. Session-wide flags (--headed, --profile, ...) go before batch; a step carrying one is refused. An explicit --observe false / --if-present false in a step overrides the batch's flag, and a step's --tab never replaces a tab the command names itself (tab close t1 --tab t2 is refused).

Read program outcome separately from transport success: a nested script with daemon-envelope data.ok:false fails its parent even if success:true. JS failures retain ok:false, return:null, error and logs. Both script forms retain observed hints in advisories (top-level in CLI script --json; data.advisories in the daemon envelope) (at most 20), including nested and failed runs; JSON op-list steps also retain each action's noProgress. Text output prints the aggregate hints once. These hints neither change action success nor retry an action.

script
# JSON op-list: value bus {{name.path}} + waitUntil/forEach/assert/return, --dry-run to validate
$ chrome-use script prog.json
$ chrome-use script - --dry-run < prog.json

# JS form: a synchronous JS program driven by cu.* helpers (ego's code-base idiom)
$ chrome-use script --timeout 120000 <<'JS'
  cu.open('https://news.ycombinator.com');
  const rows = cu.eval("[...document.querySelectorAll('.athing')].length");
  return { count: rows };
JS

Options: --timeout <ms>, --dry-run (validate the JSON program only), --yes (auto-confirm inside the script), --arg k=v (seed a variable). Exit codes: 0 ok, 1 runtime failure or failed assert, 2 invalid program.

Agent loop (experimental)

jev run drives a goal end to end: TypeSafe's Jev picks each step's operation and target from an indexed element table, and a small model writes text only when a field needs typing. Needs TYPESAFE_API_KEY (or ~/.config/typesafe/key).

jev
$ chrome-use jev run --goal "find a flight from Zurich to London" --url https://www.google.com/travel/flights

# Also ask whether the chosen action ends the goal, and record that claim
# against what the closing decision then decided
$ chrome-use jev run --goal "..." --terminal-shadow

The run reports elapsed_ms, jev_ms, text_ms, observe_ms, act_ms, fresh_ms, and decision/action counts. Measure the actual task; timing proportions from one page or provider do not generalize.

--terminal-shadow adds a completion-prediction question to the model request. It records terminal_predicted, terminal_condition_observed, terminal_confirmed_done, and terminal_refuted. It does not skip the normal completion decision. Agreement with a later model decision is not independent proof of task success; adding a question can also affect model choices and latency.

stale_by_phase and stale_fields describe marker-based freshness rejections using categories rather than raw page text. Click/select guard rejections are not included in those breakdowns. A changed URL or a control leaving the actionable set is not a completion test. These counters do not establish safely removable model calls.

JEV_TRACE=<file> appends one JSON line per decision: the request that was sent, the candidates the model was shown (id, kind, label, current value, checked state), its choice, and Jev's raw answers with probabilities. It is off unless set. It records the goal, the page text and field values, including anything already typed into the form, so treat the file as sensitive. The run report also splits act_ms into act_read_ms, cmd_click_ms, cmd_press_ms and cmd_insert_ms.

State management

state
$ chrome-use state save auth.json    # save cookies, storage, and auth state
$ chrome-use state load auth.json    # restore saved state

Init scripts

init scripts
$ chrome-use open --init-script <path>       # register before the first navigation (repeatable)
$ chrome-use addinitscript <js>            # register at runtime (returns an identifier)
$ chrome-use removeinitscript <id>         # remove a registered init script

React / Web Vitals

The react ... commands require launching with --enable react-devtools; vitals and pushstate are framework-agnostic.

react / vitals
$ chrome-use open --enable react-devtools <url>    # launch with the React hook
$ chrome-use react tree                            # full component tree
$ chrome-use react inspect <fiberId>               # props / hooks / state / source
$ chrome-use react renders start                   # start recording re-renders
$ chrome-use react renders stop [--json]           # stop and print the render analysis
$ chrome-use react suspense [--only-dynamic] [--json]  # Suspense boundaries + classifier
$ chrome-use vitals [url] [--json]                 # LCP/CLS/TTFB/FCP/INP + hydration
$ chrome-use pushstate <url>                       # SPA client-side navigation (auto-detects Next router)

Accessibility audits

a11y runs the axe-core engine embedded in the chrome-use binary. It does not fetch a CDN script, works under strict page CSP, and preserves any page-owned window.axe value. The report includes same-origin and cross-origin iframe findings with their frame selector paths. Safari and iOS WebDriver sessions are not supported because the audit requires CDP.

a11y
$ chrome-use a11y                                  # audit the current page
$ chrome-use a11y https://example.com              # navigate, then audit
$ chrome-use a11y --tags wcag2a,wcag2aa            # filter by axe rule tags
$ chrome-use a11y --selector "#main"               # scope to one subtree
$ chrome-use a11y https://example.com --json        # structured output

Text output lists impact, rule id, fix guidance, and failing selectors. JSON output contains counts plus trimmed violations and incomplete arrays. MCP clients can call chrome_use_a11y from the all tool profile.

Debugging

console / errors capture is off by default — an active CDP Runtime domain is a detectable bot signal. Start the session with AGENT_BROWSER_CAPTURE_CONSOLE=1 when you need it.

debugging
$ chrome-use --headed open example.com   # show the browser window
$ chrome-use --cdp 9222 snapshot         # connect over a CDP port
$ chrome-use connect 9222                # alternative: the connect command
$ chrome-use console                     # view console messages (needs AGENT_BROWSER_CAPTURE_CONSOLE=1)
$ chrome-use console --limit 20          # only the last 20 console messages
$ chrome-use console --level error,warn  # errors and warnings only
$ chrome-use console --filter cart --limit 5  # last 5 lines mentioning cart (filter first, then tail)
$ chrome-use console --clear             # clear the console
$ chrome-use errors                      # view page errors (needs AGENT_BROWSER_CAPTURE_CONSOLE=1)
$ chrome-use errors --clear              # clear errors
$ chrome-use diagnose                    # why is the page blank: checks asset 404/MIME + mount state
$ chrome-use highlight @e1               # highlight an element
$ chrome-use inspect                     # open DevTools for this session
$ chrome-use trace start                 # start recording a trace
$ chrome-use trace stop trace.zip        # stop and save
$ chrome-use profiler start              # start profiling
$ chrome-use profiler stop trace.json    # stop and save the profile

Find pages the user saved (find-url)

Search local Chrome/Edge bookmarks by keyword — for internal systems or previously saved pages that public search can't reach. Reads locally; no browser / daemon required. Results are sorted by most recently added, and javascript: / data: bookmarks are skipped.

find-url
$ chrome-use find-url jira board          # all keywords must match (name or url)
$ chrome-use find-url --limit 10 invoices
$ chrome-use find-url --browser edge --profile "Profile 1" wiki
$ chrome-use find-url grafana --json      # {results:[{name,url,folder}], count}

Drive your real logged-in Chrome (extension)

Chrome 136 locked down --remote-debugging-port on the default profile, so to drive the user's existing logged-in window, chrome-use goes through a Chrome extension + native messaging — no port, no token, no per-use confirmation. A one-time install writes the native messaging host manifest:

extension
$ chrome-use extension install        # write the native messaging host manifest (one-time)
$ chrome-use status                   # CLI, extension, relay, profile, and session health
$ chrome-use extension connect        # auto-attach to the real logged-in tabs
$ chrome-use tab                      # list the real tabs it now controls
$ chrome-use tab t3                   # switch the session to one of them
$ chrome-use snapshot -i              # drive it like any normal session
$ chrome-use extension status         # is the host installed?
$ chrome-use extension uninstall      # remove the host manifest

# macOS: skip the policy profile that puts Chrome in managed mode
$ chrome-use extension install --no-profile
$ chrome-use extension install --all-profiles   # open the Store page in each missing profile
$ chrome-use extension install --user-data-dir <path>   # register the host for a Chrome started with --user-data-dir (macOS/Linux; running ones are found automatically)

Extension 0.5.48 adds a guarded native-tab-group operation for Thermo. It accepts only a reviewed plan, rechecks each tab, preserves existing/current/pinned/audible/private/agent tabs, and never crosses windows or profiles. It journals before mutation, verifies the result, and supports one-shot undo without replaying uncertainty. Normal chrome-use session ownership rules are unchanged.

⚠ The macOS policy profile means a managed Chrome
The setup wizard's "silent, every profile at once" route makes Chrome "managed by your organization": the "Use secure DNS" (DoH) setting is disabled and locked (#187), and the extension becomes "installed by your administrator" with no manual update/remove button (#186). Skip it with --no-profile, or undo it with profiles remove -identifier com.leeguoo.chrome-use.connect (Chrome then uninstalls the policy-installed extension — re-add it from the Store). chrome-use extension status always reports where you stand.

chrome-use status distinguishes an installed manifest from a runnable native-host launcher. JSON output exposes the launcher result as extension.hostHealthy.

✅ Install from the Chrome Web Store (recommended)
The Store build is stable across restarts and auto-updates (store id knfcmbamhjmaonkfnjhldjedeobeafmk). A loaded unpacked dev build can be disabled / dropped when Chrome restarts, silently breaking the relay — use the Store build for unattended scenarios.
ℹ️ Security
The extension↔host link is authenticated by Chrome (the extension id); the host↔chrome-use CDP link uses an unguessable URL in a 0600 file. --extension <path> is unrelated — that loads an extension into a freshly launched browser.

Global options

These flags go before the command and apply to the whole session. Note: headless mode is forbidden (a bot signal) — it's headed by default and always (stealth); on a display-less server, use AGENT_BROWSER_ALLOW_HEADLESS=1.

global options
$ chrome-use --session <name> ...    # isolated browser session
$ chrome-use --json ...              # JSON output, easy to parse
$ chrome-use --headed ...            # default and always on (stealth)
$ chrome-use --full ...              # full-page screenshot (-f)
$ chrome-use --cdp <port> ...        # connect over CDP
$ chrome-use -p <provider> ...        # cloud browser provider (--provider)
$ chrome-use --proxy <url> ...       # use a proxy server
$ chrome-use --proxy-bypass <hosts>   # hosts that bypass the proxy
$ chrome-use --headers <json> ...    # HTTP headers scoped to the URL origin
$ chrome-use --executable-path <p>   # custom browser executable
$ chrome-use --extension <path> ...  # load an extension (repeatable)
$ chrome-use --ignore-https-errors   # ignore SSL certificate errors
$ chrome-use --hide-scrollbars false # keep native scrollbars in headless Chromium screenshots
$ chrome-use --help                  # help (-h)
$ chrome-use --version               # version (-V)
$ chrome-use <command> --help       # detailed help for a command

Environment variables

environment
AGENT_BROWSER_SESSION="mysession"            # explicit session name
AGENT_BROWSER_SESSION_ID="agent-id"           # stable runner id for auto-isolation
CODEX_THREAD_ID="thread-id"                   # recognized automatically for Codex tasks
AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # custom browser path
AGENT_BROWSER_EXTENSIONS="/ext1,/ext2"       # comma-separated extension paths
AGENT_BROWSER_INIT_SCRIPTS="/a.js,/b.js"     # comma-separated init script paths
AGENT_BROWSER_ENABLE="react-devtools"        # comma-separated built-in init features
AGENT_BROWSER_HIDE_SCROLLBARS="false"        # keep native scrollbars
AGENT_BROWSER_PROVIDER="browserbase"         # cloud browser provider
AGENT_BROWSER_STREAM_PORT="9223"             # override the WebSocket stream port
AGENT_BROWSER_DASHBOARD_ALLOWED_HOSTS="ws.example.com"  # extra hostnames the dashboard accepts (reverse proxy); loopback names only otherwise
AGENT_BROWSER_CLICK_MODE="dom"               # click strategy: ""(default) / "coord" / "dom"
AGENT_BROWSER_BACKGROUND_LINKS="cross-site"  # opt-in (#468): cross-site target=_blank links open in a background tab. unset/"off"/"0" (default) never; "cross-site"/"1"/"on"; "all" same-site too. Cost: redirect-back links lose SameSite=Strict cookies; read at daemon start

Stealth / anti-detection knobs (fork)

stealth knobs
AGENT_BROWSER_CAPTURE_CONSOLE="1"     # enable console/errors capture (off by default; the Runtime domain is a bot signal)
AGENT_BROWSER_TIMEZONE="Asia/Tokyo"   # --launch only. native timezone override (IANA id or "auto")
AGENT_BROWSER_BLOCK_WEBRTC="1"        # --launch only. hide the local IP via WebRTC
AGENT_BROWSER_HIDE_CANVAS="1"         # --launch only. session-stable canvas/audio fingerprint noise
AGENT_BROWSER_ADAPTIVE_REF="0"        # disable adaptive @ref relocation (on by default)
⚠️ console / errors return empty by default
In this stealth fork, both return {"messages":[]} / {"errors":[]} plus a hint until you start the session with AGENT_BROWSER_CAPTURE_CONSOLE=1 — this keeps the CDP Runtime domain disabled on the common automation paths. When something doesn't work, cross-check Troubleshooting.

Semantic find role/text/label/placeholder/alt/title/testid requires exactly one visible match, including locate-only queries. Ambiguity returns up to eight candidates with visible state and selector/context hints; no action is dispatched and input values are omitted. Narrow --name/--exact, or use --within <CSS|@ref> to restrict the query to exactly one container. A scope must belong to the active tab's main document; cross-frame refs and an explicitly selected iframe are refused; use direct frame refs or `frame main`. find first/last/nth explicitly selects an order and keeps its existing behavior. A plain CSS selector or XPath given to an action (click, fill, type, hover, check, select, drag …) must match exactly one element too: one that matches several is refused with code selector_ambiguous, listing the first five matches with their text and position, and nothing is done. Narrow it, use a @ref, or choose by index with find nth <index> <selector> <action>; AGENT_BROWSER_STRICT_SELECTORS=0 (read when the session's daemon starts) restores first-match for old scripts. Roles and label names use Chrome accessibility data, including aria-labelledby; a semantic query re-resolves on each call. A detached target before dispatch fails safely; uncertain actions are never replayed.

Discover a scope from the page first: find query "Beta account" returns candidate selector anchors; choose the article/container corresponding to that heading, then use find role button click --name Save --exact --within "<returned-selector>". Inspect an ambiguous scope with snapshot -s "<selector>" and narrow it rather than accepting the first container. Unknown semantic-find options and duplicate --within are refused; use find label Email fill -- --name to enter literal flag-looking text. --exact false requests substring matching.

The existing chrome_use_find tool in mcp --tools all accepts within with the same unique-scope rules; tool count is unchanged. text for fill/type is passed literally, including --name --observe, rather than parsed as CLI options.

Semantic find retains the existing --observe unsupported warning; use one batch containing the scoped find action and a task-specific get text receipt when both are known. The MCP find tool does not advertise an observe field.