SuperAPI AI Directory

chrome-agent

Agents & models

View repository

Updated 10-16 01:28

About this skill

chrome-agent — Browser Automation
which chrome-agent || npm install -g chrome-agent   # or: cargo install chrome-agent

Reading a response

Every mutating action carries verdict + verdict_reason + next. Every pointer-targeted action also carries delivery (a hit test at the coordinate about to be dispatched) and intercepted_by when another element took the event. Every write also carries value.actual beside value.requested, read back after observed_after_ms — same field and window for fill, select, check/uncheck.

ok:true means the command ran, not that the page complied. Read verdict, and read value.verbatim after any write, before reporting success. Branch on next, not on the verdict word. next is one token from a closed set of six: proceed inspect retry confirm dismiss stop.

verdict verdict_reason next What to do
changed tree_delta proceed The page moved; delta says how.
changed nodes_moved proceed Same nodes, reordered (a drag landed).
changed focus_only proceed Only focus moved, onto a real element — on a path with no hit test that is the sole sign the action arrived. focus.to is often a focusable ancestor of what you clicked; read uid for the element you aimed at. Focus onto the document answers identical_tree instead.
changed value_kept proceed / inspect The element held it when read back, and no delta could show that (a secret renders as a fixed marker; a first action has no tree to compare). fill, select and check/uncheck all reach this row; the evidence is value.verbatim. The one row whose next depends on more than the reason: if the PAGE read also failed it answers inspect — the field is confirmed, the page around it was never seen.
changed values_lost confirm It moved AND emptied a field that held a value; values_lost:[{uid,role,name,was}] names each. Submitted-and-cleared and discarded-without-sending look identical. Confirm with assert text --contains or network before re-filling — a re-submit may send the work twice.
navigated document_replaced inspect New document. Every stored uid is dead.
intercepted hit_test_receiver dismiss Another element occupied the aim point and got the event; intercepted_by names it (tag/id/class/uid/z_index/text/modal). Nothing is known about the target. This is the row you will actually get: 8 interceptions over 61 real sites, all of them this one. Read tag/id/class, not z_index — it was auto on 7 of the 8.
intercepted modal_dialog dismiss A <dialog> opened with showModal() (or a fullscreen element) holds the top layer and receives everything outside itself. Press Escape first. Rare — 0 over 61 real sites: modal is el.matches(':modal'), so the <div role="dialog"> overlays most sites ship arrive as hit_test_receiver instead.
not_kept value_reverted stop The write reached the element and it held nothing afterwards. Read value.actual. Do not fill again — the same write produces the same answer.
not_kept value_rewritten stop It holds something else — a mask, a trimmer, a normaliser. Read value.actual and decide whether that is the value you wanted.
no_effect delivered_no_change confirm Delivery proven to the target, and the tree stayed still within observed_after_ms. The strongest word available. Repeating is the one thing that cannot help.
unchanged identical_tree confirm The tree was identical while the tool watched. Delivery was not proven.
unknown no_baseline inspect First action on this page; nothing to compare against.
unknown read_failed inspect The action ran; reading the page afterwards failed.
unknown identity_unreadable inspect The two trees may not belong to the same document.
unknown aim_point_off_target inspect Two readings agreed and the point still could not be aimed at — nothing was dispatched, the miss is stable, an identical retry misses identically. aim tells the two shapes apart: on screen but outside the element's own boxes (wrapped inline box, clipped container) → aim at a child with a box; outside the viewport (a fixed wall, a locked scroll) → change the page's state, no scroll will move it.
unknown scroll_not_settled retry Two readings of the aim point disagreed: it was still moving. Nothing was dispatched, the miss is transient, and the retry is the fix. wait, then repeat.
not_checked reporting_disabled proceed You passed --verdict off. The silence is yours, not the page's.

delivery rides on pointer-targeted actions (click, dblclick, the check/uncheck click) and is what licenses the two strong words:

delivery Means Licence
target_hit The aim point resolved to the target, a descendant, its label's control, or its shadow host. The only value that permits no_effect.
intercepted The aim point belongs to an element outside the target's flat subtree. → verdict: intercepted; intercepted_by names the receiver.
off_target No point could be aimed at and two probes agreed: outside its own client rects, or outside the viewport and pinned there. Nothing dispatched, so a repeat doubles nothing — but the miss is stable, so it cannot succeed either. Hence inspect.
not_settled Two probes disagreed: the aim point was still moving when the settle budget ran out. Nothing dispatched, miss is transient, so repeating is safe and is the fix. Hence retry.
js Went through a JS click()/MouseEvent, which performs no hit test. Interception is inapplicable, not undetected. Licenses nothing.
not_probed No hit test ran, or its answer does not cover the document that matters (a target inside an iframe). Absence of evidence. Licenses no conclusion.

--on-intercept has three modes. dispatch (default) sends through any receiver, which is what a pointer does. refuse errors on every interception. guard sends through an inert receiver and refuses one that looks like a control, reading intercepted_by.actionable from the same probe call: a native interactive tag, an ARIA interactive role, tabIndex >= 0, or cursor: pointer. An <iframe> receiver always refuses under guard — its content is opaque, so "inert" could only be assumed. Any refusal is ok:false (exit 1) with the same fields as a dispatch plus dispatched:false, which appears on every response that aimed and sent nothing; next says dismiss, and re-aiming duplicates nothing.

Never report a success the tool did not confirm. unchanged is also what a click swallowed by an overlay looks like: confirm with assert before repeating, because a repeat is a second real click. unknown means the tool could not compare, never "nothing happened" — run inspect once and continue from what you see; the one exception is scroll_not_settled, where nothing was dispatched. waited_ms appears only when the action waited for a page load after it. A pointer event Chrome does not acknowledge within 8 s fails with ok:false rather than waiting out --timeout; it may already have reached the page, so run inspect instead of repeating. click, dblclick, hover and drag bring their page to the foreground first (a background tab answers pointer events on a 5 s timer).

Two blind spots, neither fixable by retrying. The observation window is fixed at 60 ms (observed_after_ms): it catches a revert on the microtask queue, in setTimeout(0) or in an animation frame, not a validator that clears the field at 400 ms — for that, wait then assert value. Canvas, WebGL and CSS-only effects are invisible to the accessibility tree: a class, an opacity, a transform, a repaint all look like unchanged or no_effect, so confirm with screenshot or eval, never with a second click.

Workflow: inspect → act → assert

chrome-agent goto https://app.com/login --inspect   # uids
chrome-agent fill --uid n52 "user@test.com"         # value: what the page kept
chrome-agent click n63                              # verdict + delivery + next
chrome-agent assert text --contains "Dashboard"     # exit 0 = evidence you may quote

Re-inspect when verdict is navigated, when next says inspect, and after any SPA route change (back, forward, a click that re-renders) — uids are reassigned. For an SPA detail page prefer goto <direct-url> over click, which may open a modal.

assert: the exit code is the answer

Prove the end state before reporting a task complete. Exit 0 is the only evidence you may quote.

chrome-agent assert value --selector "#email" --equals "ada@example.com"
chrome-agent assert text --selector "#status" --contains "Shipped"
chrome-agent assert url --matches "/orders/[0-9]+$"
chrome-agent assert state --selector "#terms" --checked        # native OR aria-checked
chrome-agent assert state --uid n15 --selected "California"    # a dropdown's current option
chrome-agent assert state --selector "#submit" --disabled      # :disabled OR aria-disabled
chrome-agent assert state --selector ".modal" --visible        # rendered, opaque, not hidden
chrome-agent assert exists --selector ".result" --count 10     # --min N, or --count 0 for absence
chrome-agent assert value --selector "#quantity" --equals "1" --within 5

Use --within N for a delayed result (positive whole seconds); assertions otherwise check once. Only reads repeat, and the first matching observation succeeds. This does not prove stability. JSON adds assertion.wait: within_ms, elapsed_ms, observations, timed_out. Expiry is exit 2 with the last comparison. Unreadable targets, blocked reads and connection loss stay exit 1, with error, wait and optional last_observation. A missing value/text/state target is still an error; wait for presence with assert exists --within N first. Pipe and macros accept "within":5 on the assertion command. The window bounds the entire read; --timeout continues to cap individual CDP calls.

  • 0 it held · 2 the condition did not hold (report or repair) · 1 the check could not finish: no browser, a selector matching nothing, an unparseable regex, a CDP timeout. A macro run guard or a discover step assertion that did not hold also exits 2. A bad flag exits 1. Inside pipe/batch an assertion has no exit code of its own: it is ok:false with the same assertion object, and a batch that stopped on it exits 1, not 2.
  • assert is a read: no change report, no verdict, and it never clicks. Secrets are compared but never printed (the response gives both lengths). --checked reads the same classification check/uncheck apply and --selected the same reading select uses, so an assertion cannot disagree with the action.
  • --matches is a Rust regex: \d \w \s are ASCII-only, no \p{...}, no lookaround; (?i) for case-insensitive. --contains is a plain case-sensitive substring.

Targeting has three modes: uid (from inspect, e.g. n47, stable across inspects of the same page), --selector (CSS), --xy (coordinates). Every targeted action returns the uid it actually resolved, even when you aimed with --selector — cross-check it against delta. click --selector is the same verb as click <uid>: both resolve the viewport centre and dispatch native input, so a --selector on a button behind a cookie banner clicks the banner. That is what a pointer does, and what intercepted_by tells you.

Commands

# Navigation
chrome-agent goto <url> [--inspect] [--wait-for "selector"] [--header "K: V"]
chrome-agent back [--inspect]                                # answers {url, title}
chrome-agent forward [--inspect]                             # at either end: a message, no url
chrome-agent scroll down|up|<uid>
chrome-agent wait network-idle [--idle-ms N] [--timeout N]   # deterministic SPA/XHR settle
chrome-agent wait text|url|selector "pattern" [--timeout N]
chrome-agent history [--filter "pattern"] [--limit N]
# Inspection — narrowing flags change what is PRINTED only; `diff` always compares the full tree
chrome-agent inspect [--max-depth N] [--filter "button,link"] [--uid nN] [--urls]
chrome-agent inspect --filter "article" --scroll --limit 50   # infinite-scroll feeds
chrome-agent inspect --max-chars 4000 [--offset 4000]         # cap/page huge trees
chrome-agent diff                                             # what changed since last inspect
# Pointer actions — uid, --selector or --xy
chrome-agent click <uid> [--inspect]
chrome-agent click --xy 100,200                           # or --selector "css"
chrome-agent dblclick <uid> [--inspect]
chrome-agent hover <uid>
chrome-agent drag <from-uid> <to-uid>                     # mouse events; NOT HTML5 DnD
# Writes
chrome-agent fill --uid <uid> <value> [--secret]           # or --selector "css"
chrome-agent fill-form n20="a@b.com" n30="password"       # one value report per field
chrome-agent type "text" [--selector "input.search"] [--secret]
# --secret reports lengths instead of the value. It only ADDS to what the element declares;
# there is no flag that turns redaction off.
chrome-agent press Enter|Tab|Escape                       # a single printable char types it
chrome-agent select --uid <uid> "Option text"             # option.value first, then visible text
chrome-agent check <uid> | chrome-agent uncheck <uid>     # idempotent; refuse what they cannot check
chrome-agent upload --uid <uid> /path/to/file.pdf         # --selector too (file inputs hide from a11y)
# Reads
chrome-agent read [--truncate N]                          # articles, via Readability
chrome-agent text [--selector "main"] [--truncate N]      # visible text
chrome-agent extract [--selector "css"] [--limit N] [--scroll] [--a11y]
chrome-agent eval "JSON.stringify(...)" [--selector "h1"]
chrome-agent network [--filter "pattern"] [--body] [--limit N]   # already loaded, stealth-safe
chrome-agent network --live 5 --body --filter "graphql"          # capture live traffic
chrome-agent network --abort "*tracking*" --live 30              # blocking; start before navigating
chrome-agent console [--level error] [--clear]
# --body with --filter fetches every match whatever its MIME; without --filter, textual types only
# Files — written 0600 under ~/.chrome-agent/tmp; path on stdout, never base64
chrome-agent screenshot [--format jpeg] [--quality N] [--max-width N] [--uid nN|--selector "css"]
chrome-agent pdf [--filename name] [--landscape] [--background]
chrome-agent download <url> [--out path] [--max-bytes N]        # in-page fetch, keeps the login
chrome-agent download <--uid nN|--selector "css"> [--out path]  # CLICK it, capture the download
# Device metrics — explicit values, scoped to one named page, no preset catalog
chrome-agent --page mobile emulate device --label "checkout phone" --width 412 --height 915 --dpr 2.625 --mobile --touch
chrome-agent --page mobile emulate status
chrome-agent --page mobile emulate reset
# Persisted per named page, reapplied on each connection; a Chrome restart discards it. Acting on
# an emulated page activates its tab and backgrounds siblings. Under --touch, click/check tap.
# WebMCP tools — document.modelContext.getTools()/.executeTool()
chrome-agent webmcp list                                          # name, description, inputSchema
chrome-agent inspect                                              # baseline first, like any action
chrome-agent webmcp call add_to_cart --args '{"item":"Espresso Blend"}'
# The protocol defines no outputSchema, so the accessibility-tree diff is the only corroboration
# for what a tool DECLARED: one that declares success and moves nothing reads
# unchanged/identical_tree. Under `frame` the response carries frame_scoped:true — an empty list
# there is unproven, not "none". Most installs lack native WebMCP: use --chrome-arg above.
# Multi-step — pipe keeps one connection, so uids stay valid across commands
echo '[{"cmd":"goto","url":"..."},{"cmd":"inspect"},{"cmd":"click","uid":"n12"}]' | chrome-agent --json batch
echo '{"cmd":"goto","url":"...","inspect":true}' | chrome-agent pipe
# Iframes — the frame switch lives on the connection, so use pipe/batch
printf '%s\n' \
  '{"cmd":"frame","target":"iframe[src*=\"checkout\"]"}' \
  '{"cmd":"inspect"}' \
  '{"cmd":"fill","uid":"n42","value":"..."}' \
  '{"cmd":"frame","target":"main"}' | chrome-agent pipe
# Separate CLI calls (frame then inspect) do NOT work — each is a fresh connection.
# frame scopes eval + inspect, NOT --selector targeting. Act by uid after switching.
# Session, tabs, and paths that already worked
chrome-agent status                                              # browsers, pids, orphan= lines
chrome-agent tabs
chrome-agent macro list                                          # named paths, with their guards
chrome-agent macro check checkout --var email=ada@example.com    # validate offline before opening Chrome
chrome-agent macro run checkout --var email=ada@example.com      # stops at the first failed guard
chrome-agent replay recording.jsonl                              # a pipe --record file, no guards
chrome-agent close [--purge] [--orphans]                         # --orphans: browsers no session claims

Macros. macro record <name> --from-recording <file> (or {"cmd":"macro","action":"record","name":"x"} inside the pipe session that just did it) distils actions, waits, assertions, reads, downloads and supported context. Discovery snapshots, diffs and history are dropped. Failed or undelivered steps and unsupported locators refuse the whole recording without saving or replacing a macro. Derived guards are delivery: target_hit, verdict: changed|navigated, verbatim: true, downloaded: true and url_matches on the path. A step aimed by uid is recorded by role + accessible name, or refused. A secret field becomes a declared parameter and is never stored, so macro run refuses without --var. A guard that does not hold stops the run, naming the step index, the guard, what was observed and the action's own next, and exits 2 with stopped_by: "guard" — the same code as a failed assertion, because it is the same kind of claim. A run that stopped for any other reason (the step failed, the page could not be read, no such macro) exits 1 with stopped_by: "error". Steps that could promise nothing are marked unguarded and counted in both reports — read that number before trusting a green run. Explicit assertions and waits count as checks.

macro check prepares all commands, parameters and guards offline; macro run uses the same preparation before any action. Repeat --var for multiple inputs; commas remain part of a value. Substitution visits string values in commands and guards once. CSS, JavaScript and live-page conditions are checked during replay. Use --json to retrieve each completed steps[].result and the stopped step's result, including data, assertion evidence and download paths. Declared secret inputs are redacted. Task success requires explicit outcome assertions supplied by the caller.

Discovery that survives a new conversation

Use discover when exploring an unfamiliar task and another process may need to continue. You supply the reasoning; the CLI persists the objective, hypotheses, unknowns and command responses. It does not infer task completion from a successful browser command.

chrome-agent --json discover start discovery.json --goal "Read the entry page" --url https://example.com --max-commands 10
chrome-agent --json discover step discovery.json --proposal '{"id":"open","revision":0,"reason":"Observe the entry page","command":{"cmd":"goto","url":"https://example.com"},"checks":[{"cmd":"assert","what":"exists","selector":"h1"}]}'
chrome-agent --json discover show discovery.json
chrome-agent --json discover export discovery.json --name entry-page --steps open

Use the returned revision for your next proposal and the browser/page names supplied at start. Identical proposals with the same ID retrieve earlier receipts (replayed:true); changed proposals under that ID are refused. A process loss leaves a pending experiment whose outcome is uncertain. Read the record and submit a new observation; do not blindly repeat the old step.

The initial profile permits same-origin goto, inspect, read, text, extract, assert and wait. The first experiment must navigate to the entry URL. Defaults: 50 commands and 1,800 seconds including time between calls. Checks and failed or unresolved attempts consume budget. Responses above 64 KiB stop with uncertainty; narrow the next read. The file contains raw inputs and observations and stays local. Site JavaScript can still cause external effects; this command profile is not a sandbox for untrusted recipes.

Export selects successful experiments in order, requires a starting navigation and final assertion, refuses document uids, and creates a new macro name. It preserves parameter templates without input defaults. The candidate still needs fresh-input replay and independent result checks. Legacy macro run does not enforce discovery origin or budgets. discover step exits 0 for observed commands/checks, 2 for an assertion that did not hold, and 1 for error or uncertainty. start, show and export never open Chrome.

Global flags

Accepted on either side of the verb. Two exceptions: --timeout and --max-depth. A command that declares its own takes it after the verb (wait selector ".x" --timeout 5); everywhere else they must come before it. Passing one where it is not declared is an exit-1 usage error naming the invocation that works.

--stealth          # 7 anti-detection CDP patches
--json             # structured JSON on stdout; errors exit 1 with {"ok":false,...} still on stdout
--browser <name>   # isolate parallel agents — sharing "default" corrupts sessions
--page <name>      # named tabs
--max-depth N      # limit inspect tree depth
--verdict MODE     # auto (default): report what changed. off: report the action only
--budget N         # cap the change report, in chars (default 1200; 0 = uncapped)
--on-intercept M   # dispatch (default) | guard | refuse
--timeout N        # deadline on every CDP call (default 30s)
--headed           # show the window
--connect URL      # attach to a real Chrome (DataDome/Kasada)
--proxy-server URL # http(s)/socks4/5://host:port; launch-only
--copy-cookies     # use cookies from your real Chrome profile
--chrome-arg FLAG  # extra flag for the launched Chrome (repeatable); launch-only
--dialog MODE      # accept (default) | dismiss | manual · --dialog-text for prompt()

--chrome-arg applies only when chrome-agent launches Chrome (refused, not ignored, under --connect) and is fixed for the life of a named browser — close or purge it to change them. Refused outright: --user-data-dir, --remote-debugging-port, --remote-debugging-pipe, --proxy-server (use the global one), --headless (use --headed).

Exit codes: 0 success · 1 error (including a bad flag, and a batch --stop-on-error that stopped) · 2 an assertion (including discover step) or a macro run guard did not hold · 130 Ctrl+C. JS dialogs auto-accept so the page never hangs.

Bot protection

Site protection Solution Measured
None chrome-agent goto ... —
Cloudflare JS challenge ("Just a moment…") chrome-agent --stealth goto ... shop.app: 403 / 6 nodes without, 200 / real page with
Cloudflare managed Turnstile --stealth does not get through nowsecure.nl: 10 nodes with and without
Logged-in sites (X, Gmail) chrome-agent --stealth --copy-cookies goto ... —
DataDome/Kasada google-chrome --remote-debugging-port=9222 & then chrome-agent --connect http://127.0.0.1:9222 goto ... leboncoin.fr: 403 with --stealth and without

The two Cloudflare rows are different products: "Just a moment…" is a JS challenge the seven patches satisfy, while a managed Turnstile fingerprints the browser and no in-binary patch moves it. There, and for DataDome, --connect to a real Chrome is the only route. Read the page after navigating rather than assuming the flag worked.

Response shapes (--json)

Success: {"ok":true, ...}
Error:   {"ok":false, "error":"message", "hint":"what to do next"}
  • goto → {"url","title","landed":{"requested","final","redirected","http_status","serving","challenge_from"?}}. Check redirected: an expired session bouncing to a login wall otherwise reads as a successful load. Fragment-only and trailing-slash differences are not redirects. http_status is the final hop's, absent (never 0) when the browser reports none. A redirect onto /login, /signin, /auth, /sso adds a hint guessed from the URL. goto carries no verdict.

  • serving is what answered, and it is the field to branch on.

    serving means do
    page nothing measured contradicts the load proceed
    challenge an anti-bot vendor's frame or script is here and the site has no form of its own; challenge_from names the vendor host --connect to a real Chrome — --stealth does not defeat these
    error the server answered 4xx/5xx; read http_status fix the URL (404), authenticate (401/403), or wait (429/5xx)
    nothing_actionable no link, no form control, no script, almost no text inspect before giving up
    unreadable the shape probe did not run inspect

    page is the absence of evidence, not a certificate: a paywall, a cookie wall and an unknown captcha vendor all read as page. nothing_actionable is a measurement, not a claim you were blocked — an edge-served refusal and a page that had not finished rendering look identical.

  • click/fill/select/check/… → {"message", "uid", "verdict", "verdict_reason", "next", "verdict_hint"?, "changed":{"added","removed","changed","unchanged","moved","anonymous", "document_changed","identity_known"}, "delta":"+ uid=n88 heading \"Saved\""}, plus "focus":{"from","to"} when focus moved (deliberately not counted as a content change).

  • fill also → "value":{"requested","actual","verbatim","observed_after_ms","caveat"?}. A secret field (type=password, or autocomplete naming a password/card/CVC/one-time code) reports {"redacted":true,"requested_length","actual_length","verbatim"} instead. caveat when the write exceeded maxlength. fill-form/fill_and_submit → "values":[...], one per field, redacted the same way; on fill_and_submit it is the only witness, since the change report runs after the form has moved on.

  • select → "value" with the option TEXT; check/uncheck → "value" with checked/unchecked (indeterminate for a mixed native box). Both put observed_after_ms at the top level and both REFUSE (non-zero exit) when the page reverts inside the window. value is absent when the element already held the state — nothing was dispatched, so that response answers no_baseline/identical_tree and is no evidence this action changed anything.

  • pointer-targeted actions → "delivery", "aim":[x,y], "intercepted_by":{...} when intercepted.

  • inspect --max-chars → {"total_chars","truncated","next_offset"} · read/text → {"text"} · eval → {"result"} · network → {"requests":[...]} · console → {"messages":[...]} · batch → {"results":[...]} · screenshot/pdf → {"path"} · download <url> → {"via":"fetch","downloaded","path","bytes","mime"}.

  • download --uid/--selector clicks and captures the browser-native download — the only route to a file the page builds itself (Blob) or serves from a POST no anchor names. Branch on downloaded, never on ok: a click that landed and produced no file answers ok:true with downloaded:false and a hint forbidding the retry. --timeout bounds the whole window (begin AND finish); --max-bytes cancels past it and removes the partial file. Exactly one target — a URL, --uid or --selector; none or two is a usage error on stderr, exit 1, before a browser is resolved.

Cheap reads: choose the right one

Reach for extract first on anything that looks like a list. Figures measured on the Hacker News front page; they scale with the page, so treat them as relative.

Tool When Tokens on HN
extract Repeating records: grids, feeds, tables, results. No selectors. ~1,570 for all 30
read Articles, blog posts, product pages (Mozilla Readability) ~950
text --selector "main" Scoped visible text ~1,040 unscoped
network --filter "api" The page fetched its data over XHR — take the payload often the cleanest
eval "JSON.stringify(...)" Attributes a record view doesn't carry varies
inspect --filter "button,link" Finding what to act on, not reading content ~1,735
inspect Full tree with uids ~5,650

extract --scroll scrolls and waits on a MutationObserver for lazy content; extract --a11y works on React SPAs (X.com) where the DOM carries no stable structure.

Budget: prefer inspect over screenshot (a screenshot gives no uids, so you inspect anyway); narrow with --filter "button,link", --max-depth 2, read --truncate 1000, text --selector "main" --truncate 500; --budget N caps the change report and --verdict off removes the post-action read entirely, and every guarantee at the top of this file with it; use pipe/batch for multi-step flows — one connection, uids stay valid, and about 12 ms of per-command overhead goes away (measured 1.5x on a read stream, 1.1x on fills and clicks). Reach for it for the stable uids, not for the speed.