#33: some sites prepend runs of ZWJ/word-joiner/invisible-times/BOM to
document.title (badging/anti-scrape); left in, they polluted 'tab list', broke
text matching, and wrecked column alignment. sanitize_title() now strips
zero-width/bidi-format chars at every title ingestion point + get_title().
#34: 'screenshot <selector>' (element capture) already worked but was
undocumented; added 'screenshot --clip x,y,w,h' for an explicit pixel region
(CDP captureScreenshot clip), documented both in --help. Verified live.
chrome.debugger forbids DOM.setFileInputFiles, so 'upload' used to hard-fail on
the relay and push users to a --launch/direct-CDP session. Now it falls back to
reconstructing the File entirely in the page (Playwright/Cypress-style: build a
File from the bytes, assign input.files = dataTransfer.files, fire input/change;
for drop/paste composers like X, dispatch synthetic paste+drop with the
DataTransfer). The bytes are streamed in <1 MiB base64 chunks because the relay
tunnels CDP through native messaging (1 MiB/message cap) — a whole image as one
arg closed the channel. Verified live over the relay: an 809 KB PNG lands intact
on a file input with change firing. No more direct-CDP needed for uploads.
Passing a Cloudflare challenge mints an HttpOnly cf_clearance cookie bound to
IP+UA. 'chrome-use cf-status' (aliases cf/cloudflare-status/clearance) reports
whether the active page is currently a CF challenge and whether a still-valid
cf_clearance exists (read via CDP — HttpOnly is invisible to document.cookie),
plus CF_VERIFIED_DEVICE trust, and a recommendation: proceed (already cleared,
don't re-solve) / solve (challenge up, no clearance) / reissue (clearance present
but page still blocks → IP/UA drifted). Lets an agent avoid re-solving what it
already cleared — the persistence optimization. Pure helpers unit-tested; live
-verified on a real cf_clearance.
Driving a busy real Chrome via the relay, a single Target.getTargets call
occasionally returns a different window's tabs ('tab list hops windows'). resync
pruned every tracked page absent from that snapshot — including the agent's
explicitly-adopted (pinned) tab — after which active-target resolution fell back
to active_page_index and eval/click/snapshot drifted onto a foreign tab
(about:blank / chrome-extension:// / the user's page), breaking any 3+ step flow.
prunable_target_ids() now protects the pinned active target from snapshot-based
pruning; a genuine close still arrives as Target.targetDestroyed (event drain) and
removes it properly. Unit-tested.
The consent modal only appears on a raw remote-debugging attach or a browser we
launched with a debug port — never on the ab-connect extension relay. Append one
line per connection to ~/.chrome-use/connect-mode.log (relay | raw-port-attach |
launched | remote-ws), flagging 'CONSENT-MODAL-RISK' when a raw-port/launch path
runs while the relay was available. Lets us tell a code regression from Chrome's
own extension-debugger consent UX when the modal reappears. Best-effort, never
fails a connection. Verified: normal 'open' logs mode=relay (consent-free).
Some injected UI (browser-extension debug panels, web components) renders into a
CLOSED shadow root that eval/innerText cannot read. --pierce walks the CDP DOM
tree (DOM.getDocument depth:-1 pierce:true), which includes closed shadow roots
and child documents, and collects text nodes (skipping script/style/etc).
Review-safe: rides the per-tab debugger session already attached, no new Chrome
permission and no ab-connect/extension change — so it works in extension-relay
mode without touching the published extension. Verified live: a closed-shadow
panel that main-world eval reports HIDDEN is read in full via --pierce.
First slice of #30 (read extension/injected-panel content). Deeper extension
introspection (background SW / chrome.storage) stays a launch-mode / raw-CDP
concern, deliberately NOT done by expanding ab-connect's debugger powers.
So an agent never silently misses iframed content (listing descriptions etc.)
without having to know the --all-frames flag. Single-frame pages are unchanged
(identical to the old body read); multi-frame pages now include child frames —
a strict superset. Skill + help updated to make the default and 'frames'/--main
discoverable.
The changelog step runs under 'bash -e'. section() returned non-zero when a
commit category was empty (grep no-match / empty [ -n ] test), aborting the
script before the closing heredoc delimiter — so any release whose range lacked
a whole category (e.g. 1.5.2: only feat, no fix) failed to attach binaries.
Add '|| true' + 'return 0' so section() always succeeds.
- chrome-use sessions: top-level alias for the daemon inventory (the skill
advertises sessions, so it's a natural guess that used to error).
- Unknown commands now suggest the nearest valid one (Levenshtein + prefix
match), staying silent when nothing is close (e.g. 'clik' -> click,
'sesions' -> sessions, 'xyzzy' -> no suggestion).
- tab <id>: probe the switched session and show a warning indicator instead of
a green check when it isn't responding yet, so a switch onto a re-attaching
(churned-tabId) session no longer reports false success. The #24 targetId
recovery self-heals within ~6s, hence a warning rather than a hard error.
On listing/marketplace pages (Yahoo Auctions, Rakuten, Mercari shops) the
seller's description lives in a child frame or under a related-items sidebar,
so 'get text body' returned only header/nav boilerplate.
- get text --all-frames: aggregate visible text across every reachable frame.
Same-process child frames are read via Page.createIsolatedWorld; OOPIFs via
their auto-attached debugger session (iframe_sessions). Each non-top frame is
labelled with a '----- frame [kind] url -----' separator.
- get text --main: readability-lite — prefer the densest <main>/<article>
region over the whole body, dropping global header/nav/footer chrome.
- frames: enumerate frames (kind + url + per-frame text length) so an agent can
see where a page's text actually lives and pick the right read.
Verified live: inline srcdoc frame text aggregated through --all-frames; Yahoo
Auctions <main> (2881 chars) extracted via --main, stripping the Yahoo header.
Release notes were a flat list of commit subjects — hard to tell at a glance what
was added vs fixed (recurring '看不出改了什么'). Group by conventional-commit type
so every future release auto-shows scannable Features/Fixes sections.
Live-reproduced #24 on 0.4.8 driving the Mercari signin token-exchange hop
(login.jp.mercari.com): the cross-process nav gives the tab a NEW Chrome tabId
while the CDP targetId stays the same. So cb-tab-<oldTabId> can't be recovered —
recoverSessionTab parsed the old tabId, chrome.tabs.get(oldTabId) failed (gone),
and it gave up → permanent 'stale sessionId ... its tab is gone' until the page
settled ~6s later and something re-attached. current/tab <targetId>/daemon
restart all failed because the relay still mapped the targetId to the dead
session.
Fix: remember each session's targetId across detach (sessionTargets map). When
recoverSessionTab can't recover by the encoded tabId, fall back to the STABLE
targetId — chrome.debugger.getTargets() to find the tab now hosting that target,
attach it, and ALIAS the dead cb-tab-<oldTabId> session to the live tab so the
daemon's session id keeps resolving. Longer retry window (~6s) since this hop
takes seconds to settle. Builds on 0.4.6/0.4.8 reattach; covers the tabId-churn
case those missed.
Needs dogfood on the real Mercari flow (can't repro the tabId churn synthetically).
#25 — fill() didn't fire the events framework inputs / site autocomplete need:
it set value directly (bypassing React's value-tracker) and typed via
Input.insertText, so controlled components and input/change/blur listeners (e.g.
Mercari's postal-code → 都道府県 lookup) never ran though the value showed. fill
now emulates a real edit: focus, set through the element's prototype value setter
(React _valueTracker registers), then dispatch input → input → change → blur/
focusout. SELECT and contenteditable handled too. type <sel> <text> remains for
per-keystroke sites. Verified live: an input wired with input/change/blur fired
'IICB' from one fill.
#26 (ergonomics):
- 'close <tab>' now closes just that tab and prints 'Tab [tN] closed'; bare
'close' still closes the browser. Previously 'close t12' ran a browser close
and alarmingly printed 'Browser closed'.
- new 'chrome-use current': prints the active tab's stable handle (tabId + CDP
targetId + url/title), refreshed live — so an agent holds the targetId (which
survives cross-process nav) instead of re-deriving 'which tab is live' from
'tabs' every step. The deeper tab-id churn is the #21/#23 stable-targetId story.
Tests cover fill events (live), close tab-vs-browser parse, and current.
A click on a target=_blank link / window.open opened a new tab, but the active
tab stayed put, so the post-click snapshot showed the OLD page — looking exactly
like the click failed. On the relay the new tab is discovered only via getTargets
(the relay doesn't push target events to the daemon), so it went unsurfaced.
handle_click now snapshots tracked targets before the click and, after, runs a
lightweight BrowserManager::adopt_newly_opened (one getTargets, attaches only the
new target — far cheaper than a full resync) to detect a freshly-opened tab. It's
reported as openedTab {tabId,url,title} in the response (and a '→ opened new tab
[tN] <url>' hint in text mode). Default keeps focus on the current tab (so
multi-tab flows aren't hijacked, per #7/#8.1); 'click <sel> --follow' switches to
the new tab. Verified live: clicking a _blank link prints
'→ opened new tab [t13] https://example.org/'.
Completes the #24 friction items (B/C/D shipped in 770708b).
Three CLI gaps surfaced driving a Mercari signup→checkout flow:
- #24-B (correctness): a bare label like 'click 購入手続きへ' was fed straight to
document.querySelector as CSS and failed as an invalid selector, even though
snapshot listed the button by that exact name. build_find_element_js now tries
CSS first, then falls back to matching an interactive element by visible text
(exact then contains) — nested and non-ASCII labels resolve. 'text=<label>'
forces the text path. CSS still wins when it matches.
- #24-D: 'get text' with no selector now returns the whole page (body).
- #24-C: 'tab <ref> --activate' (alias --front) switches to the tab AND raises it
to the foreground — to surface a specific tab for the human.
Tests cover the text fallback / text= / xpath builder, body default, activate
flag. The core stale-sessionId-after-cross-process-nav bug is the #20/#23 class,
already fixed in ext 0.4.8 — needs that extension deployed.
Dogfooding (driving a canvas game) showed the slow, low-fidelity way — one
screenshot + one CLI call per action — when chrome-use already ships the right
tool: the session WebSocket is BIDIRECTIONAL. It streams ~60fps screencast frames
AND accepts input_keyboard/input_mouse/input_touch on the same socket, straight
to CDP Input.dispatch* — verified live over the extension relay (217 frames in
3.4s, ~64fps, and the input drove the game). But the inbound input protocol was
undocumented, so agents default to the CLI-per-action grind.
Document it in --help (stream) and the core skill: the frame + input message
schemas and the 'connect once, read frames, send timed input' loop, with a node
snippet. Reserve screenshots for one-off checks; use the WS for sustained
real-time control.
Dogfooding by driving a canvas game surfaced that per-action shell round-trips
(keydown; sleep; keyup) are the slowest, lowest-fidelity way to drive anything
timed — each is a process spawn + relay round-trip with ~250ms jitter, so a
'0.8s hold' is anything but.
- 'press <key> --hold <ms>': keyDown, wait, keyUp all inside the daemon, so the
hold duration is precise and it's one round-trip. For games (hold-to-move/
charge) and any press-and-hold.
- Documented the real driving pattern in the core skill + --help: script a timed
sequence in ONE round-trip with 'batch "press d --hold 900" "press j" "wait 200"'
(batch sends each step to the running daemon; --hold/wait block in-daemon), and
prefer reading engine state via main-world 'eval' over guessing from pixels.
Parser test covers plain/held/missing-duration. Builds on the keydown/keyup full
descriptor fix.
claude-in-chrome completes the Rakuten cart→購入手続き→checkout flow that
chrome-use 1.2.3 couldn't, because it binds to the browser-level tabId (survives
renderer-process swaps) rather than a CDP target/sessionId (torn down by the
cross-origin OAuth/SSO nav). chrome-use's relay is already keyed to the stable
tabId (cb-tab-<tabId>, #17) and sends commands by {tabId} — the gap was purely
that the extension treated the session→tab map as the source of truth and only
reactively re-attached after a failed lookup.
Make the tabId the PRIMARY resolution path: derive it straight from the session
id (tabIdFromSession), ensure-attach with short retries across the swap window
(recoverSessionTab now loops), and route every send through sendCdpToTab, which
on a detached-style error drops the stale handle, re-attaches the stable tab, and
retries once. So a cross-process nav never surfaces as a hard error — there's no
'session gone' window, matching claude-in-chrome. Builds on 0.4.5/0.4.6 reattach;
makes it primary + bulletproof rather than a fallback.
`keydown`/`keyup` dispatched a minimal Input.dispatchKeyEvent carrying only
{key}, so games/handlers that read event.code ("KeyD", "ArrowRight") or
event.keyCode saw nothing — a held key set no movement flag and the player
barely moved (dogfood: Dead Cell). They now build the same descriptor `press`
uses (key + code + windows/nativeVirtualKeyCode + printable text on down) via a
shared interaction::dispatch_single_key. Verified live: holding a direction now
drives continuous movement (player ran into an enemy and took damage), where
before it nudged ~80px.
The upgrade story spanned four parts (CLI, daemon, extension, skill) with no
single view and — worst — the extension was a total black box: nothing reported
which build was live, so a user could sit on a stale extension with zero signal.
- ext (0.4.7): on connect the extension sends a `hello` with
chrome.runtime.getManifest().version; the native-messaging host records it to a
`relay-ext-version` sidecar (next to relay-cdp-url, removed on exit).
- build.rs embeds the shipped extension version (AB_CONNECT_VERSION, read from the
ext manifest at compile time) so the CLI knows what extension it expects.
- `chrome-use doctor` gains a Versions section: CLI (vs the cached latest from the
background update check), extension (connected version vs the bundled expected —
warns + tells you to reload it in Chrome if behind), and skill (bundled, version-
locked; `skills add` copies may be stale). Daemon coherence was already covered.
So 'which of the four parts is on what version, and what needs upgrading' is now
one command. Verified: doctor warns on a simulated old extension and passes on a
current one; gracefully shows 'not connected / predates reporting' when the host
hasn't learned a version yet.
Dogfooding a canvas game over the extension relay surfaced three issues:
1. (serious) A fresh relay session's first `open` navigated one of the USER's
existing tabs instead of opening its own — in testing it replaced a
half-filled form with the target site. On connect the daemon passively
attaches to the user's tabs and pinned one as active; navigate() then drove
it. Now: on the relay (agent_group set), if the active tab isn't one this
session created, navigate() opens its own tab in the session's group first.
Off the relay (a browser we launched) reusing the active tab stays correct.
Pure helper active_index_is_owned() + regression tests.
2. (discoverability) `keydown <key>` / `keyup <key>` (hold-to-move, essential
for games/shortcuts) already existed as commands+daemon handlers but were
absent from --help and the skill, so they were undiscoverable. Documented in
--help, the core skill, and the canvas-app hint.
3. (UX) Canvas/WebGL pages expose almost no a11y tree, so `snapshot` is empty
and agents get stuck hunting refs. snapshot now detects a viewport-dominating
canvas with a sparse tree and prints a hint pointing at the screenshot +
coordinate-click + keydown/keyup path.
Verified live over the relay: `open` now lands the game in its own new tab with
the user's tabs (incl. the Rakuten recovery form) untouched; the canvas hint
fires on the game page; `close` cleans up only the session's own tab.
Multi-session over one relayed Chrome had a tab-identity fracture: each daemon
discovered targets ONCE at connect and assigned its own t<N> indices, so a tab
filled in session A was unreachable from session B — B saw a disjoint/blank set
and rebinding via 'open' piled up duplicate tabs. A stranded, still-filled tab
could not be finished from any other session.
- 'tab list' now re-syncs the live target set on every call: adopts tabs other
sessions opened (or that re-attached after a cross-process nav), drops gone
ones (clears phantom rows), and refreshes url/title from each live tab via
Target.getTargetInfo (the relay only stamps target_info on attach, so it goes
stale/blank after navigation — which made rows indistinguishable).
- 'tab list --full' now prints each tab's stable CDP targetId. Unlike t<N>
(per-session, reassigned each connect), targetId is stable across every session
on the relayed Chrome.
- 'tab <targetId>' adopts a specific pre-existing tab — including another
session's — WITHOUT reloading, so a half-filled form survives. handle_tab_switch
resyncs first, then resolves a raw targetId before falling back to t<N>/label.
- 'open <url> --reuse-tab' (alias --reuse) switches to an existing tab already on
that URL (matched by origin+path, ignoring volatile query/fragment) instead of
spawning a duplicate.
Verified live over the extension relay: a fresh session's 'tab list --full' lists
the user's real tabs with correct titles + full URLs + targetIds, and
'tab <targetId>' lands on and reads the exact stranded Rakuten account-recovery
form from the report. Unit tests cover URL normalization + --reuse-tab parsing;
full suite green. Docs: --help Tabs section + core skill multi-session guidance.
When a tab navigates across render processes (e.g. an SSO redirect to another
origin like login.account.rakuten.com), the debugger handle detaches and the
session drops out of the relay maps, so the next command dead-ends with
'stale sessionId ... its tab is gone' — even open/navigate, which should always
be able to drive the tab. But cb-tab-<tabId> encodes the STABLE Chrome tabId
(#17), and the tab itself usually survives the nav.
So before throwing, recoverSessionTab() parses the tabId out of the session,
checks the tab still exists + is eligible, and re-attaches (attachTab re-mints
the identical cb-tab-<tabId> session, keeping the daemon's binding valid), then
the in-flight command retries against the recovered tab. Complements the 0.4.5
onDetach proactive re-attach: that heals on the detach event, this heals lazily
on the next command if the event was missed. Falls back to the original error
only when the tab is genuinely gone (closed/restricted).
A mid-session 'chrome-use upgrade' (or a crashed worker) can leave per-session
daemons holding stale/cross-leaked tab handles, and the only fix was hunting
PIDs with pgrep/kill. Add a first-class command:
- 'daemon restart' kills every session daemon worker (SIGTERM→SIGKILL +
sidecar cleanup) but leaves the Chrome-launched __nm-host bridge alone, so
the extension relay stays up — the next command spins a fresh, clean daemon
against the same live Chrome. Closes no tabs.
- 'daemon status' lists running session daemons (pid + version) and relay state.
Wires connection::restart_all_daemons(), skips the command in the update-notify
nag, documents it in --help and the core skill. Unit tests cover the empty case
and a live-session kill (spawns a real child, asserts it's reaped + sidecars
cleaned). Issue #20.
v1.2.3's stable per-tab session id (#17) fixed sessionId STABILITY, but nothing
re-attached when an origin swaps the render process (e.g. the
login.account.rakuten.com SSO redirect — full-page nav + OOPIF). chrome.debugger
detached, the tab survived, but only onUpdated('complete') could re-attach — and
for that flow it didn't, so the session went permanently stale (even
open/navigate failed, retries didn't recover).
onDetach now proactively re-attaches the surviving tab (retry w/ backoff for the
swapped-in process to settle; skips user/DevTools-initiated detaches), so the
stable cb-tab-<tabId> session is restored and commands self-heal. Extension
0.4.4 → 0.4.5; needs a Web Store republish + dogfood on the Rakuten flow.
The changelog step lived in the separate `release` job (needs: build), which
had no checkout — so git ran with no repo ('fatal: not a git repository') and
the body came out empty. Add a fetch-depth:0 checkout to that job; drop the
now-pointless fetch-depth:0 from the build job.
Two SPA-SSO debugging gaps:
- The core skill referenced `bringToFront` but the CLI parser never mapped it
(the daemon handler existed) → 'Unknown command'. Wire up
bringToFront / bring-to-front / bringtofront → the existing action.
- 'stale sessionId — re-open your target URL' recovery was impossible because
`tab list` truncates long URLs with '…', cutting client_id/state out of SSO
links. Add `tab list --full` (also `tab --full`) to print untruncated URLs;
SKILL.md documents the recovery (full URL + re-open the stable entry URL).
The stale-session itself auto-recovers via the stable per-tab relay session id
(#17, extension 0.4.4). Parse tests for both new forms; verified live.
v1.2.2's auto-changelog came out empty: in a detached-HEAD tag checkout the
tag refs git describe/git log need aren't reliably present even with
fetch-depth:0. Fetch them explicitly first.
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
- feat(cli): non-blocking update-available notice (stderr, once/day, opt-out) so users learn to upgrade
- ci(release): auto-generate changelog from commit log on every release
The CLI ships as a GitHub Release binary with a manual `chrome-use upgrade`,
but nothing told users a newer version existed — so releases didn't reach them.
Add a lightweight update check: each run reads a cached latest-version and, if
it's newer than the running binary, prints a one-line hint to STDERR (never
stdout, so --json stays clean): "⚠ chrome-use X.Y.Z is available — run
chrome-use upgrade". The cache is refreshed at most once a day by a DETACHED
`__update-check` worker (curl → GitHub latest release), so the current command
never waits on the network. Skipped for meta commands (upgrade/install/doctor/
__*/--version/--help), in CI, in daemon mode, and via
CHROME_USE_NO_UPDATE_CHECK / AGENT_BROWSER_NO_UPDATE_CHECK.
Verified: nag shows for a newer cached version, suppressed by the opt-out env +
on meta commands + when up-to-date; the detached worker writes the real latest
tag from the GitHub API.
GitHub's generate_release_notes only lists merged PRs — near-empty for this
commit-to-main repo, so releases still showed nothing. Render the
conventional-commit subjects since the previous tag instead, and full-clone
(fetch-depth:0) so the diff is available.
GitHub Releases had an empty body — you couldn't tell what changed between
versions. Add generate_release_notes:true so every release ships an
auto-generated changelog.
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
- fix(relay): pin active target on open so commands don't drift tabs (#14, #18)
- fix(connect): stable per-tab relay session id — re-attach auto-recovers (#17)
- docs: chrome-use test in README
When a tab's chrome.debugger session was torn down and re-established
(cross-process navigation, MV3 service-worker restart wiping the in-memory
maps, DevTools stealing the debugger), the extension minted a brand-new
monotonic `cb-tab-N` for the same tab. The daemon stays bound to the old id and
the relay consumes attach/detach events without telling it to rebind, so the
session was orphaned permanently → `stale sessionId / tab is gone`, and re-open
never recovered.
Derive the session id from the STABLE Chrome tabId (`cb-tab-<tabId>`) instead.
Any re-attach of the same tab now restores the SAME session the daemon already
holds, so eval/snapshot transparently follow the new page after a navigation.
Extension 0.4.3 → 0.4.4. Adds a relay unit test for the detach→reattach-same-
session recovery contract.
Turn repetitive browser checks into unit-test-style YAML suites. Steps reuse
chrome-use's own commands; assertions (url/visible/hidden/text/count/eval)
compile to a single truthy `eval`. The runner re-invokes the binary per step
(inherits all flag/launch/daemon/ref semantics; the daemon stays up so each
step is a fast socket call), launches an isolated browser by default, captures a
screenshot on failure, and exits non-zero for CI. `setup: account:` injects a
cookie-use login. Ships a `test` skill (skills get test). Unit-tested step/assert
compilation.
#16: handle_screenshot now returns a canonicalized ABSOLUTE path, so the
`✓ Screenshot saved to …` line is the same regardless of process cwd and the
agent can read the file without guessing the cwd.
#15: `session list` now reflects the extension-relay connection — when the relay
is up it shows the active session as `(relay/extension → live Chrome)` instead
of "No active sessions", and the --json output gains a `relay` bool. Stops agents
misjudging a live relay connection as down.
DOM.setFileInputFiles is forbidden by Chrome's chrome.debugger API, so upload
always fails over the extension relay with an opaque -32000 "Not allowed". Map
it to a clear message: file upload needs a --launch/direct-CDP session, and
point at the cookies export|set --curl workaround. Note the limit in the core
skill upload line too.
The CWS rename to chrome-use updates the EXISTING item (knfcmbam…) with a
key-stripped package, not a new key-locked item — existing users auto-update and
reviews are kept. Native host allow_origins already lists both ids so the relay
doesn't break. Also point the icon/screenshots section at the generated assets.
Stage 1 of the agent-browser → chrome-use extension migration: the CLI now
writes a host manifest under both com.agent_browser.connect (extension ≤0.4.2)
AND com.leeguoo.chrome_use (the rebrand 0.5.0+), both pointing at the same
launcher, and host_installed()/uninstall recognize both. So the relay works no
matter which extension version a user has, with no forced re-install — which
lets the store roll 0.4.3 (cosmetic name only, host unchanged) and later 0.5.0
(new host) without ever breaking the relay or re-popping the consent dialog.
N parallel `open --profile <same>` (e.g. chatgpt-imagegen's web backend firing
3 image gens at once) collided on the profile-copy disk I/O and Chrome's profile
lock: every candidate burned its full ~30s launch timeout and ALL failed (0
success), because the loser instances hung without writing DevToolsActivePort.
ProfileLaunchLock takes a cross-process flock on a per-resolved-profile lock
file, held across the copy + launch until Chrome is up, so concurrent
same-profile launches queue instead of colliding — the storm becomes
all-succeed-serially instead of all-fail. The kernel releases the lock when the
holder exits, so a crash can't wedge the queue; acquisition is best-effort
(launch proceeds unlocked if it can't be taken). Uncontended single launches
are unaffected.
After the agent-browser → chrome-use rename, the new binary used ~/.chrome-use
+ host com.leeguoo.chrome_use and couldn't find the relay that the still-old
native-messaging host wrote to ~/.agent-browser → it fell back to raw
--remote-debugging-port and re-popped 'Allow remote debugging?'.
- config_home()/config_dir_basename(): decide once per run — prefer the new
.chrome-use, but keep using an existing .agent-browser install if that's the
only one present; fresh installs get .chrome-use. get_socket_dir() routes
through it so sockets/state are consistent within a run.
- relay_url_path(): the relay-cdp-url is a cross-binary handoff (host writes,
CLI reads), so read from whichever brand dir actually has the file
(~/.chrome-use OR ~/.agent-browser).
Combined with keeping HOST_NAME=com.agent_browser.connect (b6febbe), the renamed
chrome-use binary now relays through the existing ab-connect 0.4.2 extension
with zero dialog. Verified live: chrome-use found ~/.agent-browser/relay-cdp-url
and listed the user's real tabs, no consent dialog.
Reverting the host-name rename from the chrome-use rebrand. The host name is
invisible internal plumbing (lives only in NativeMessagingHosts/*.json and the
extension), so renaming it to com.leeguoo.chrome_use bought nothing user-facing
but broke the relay for every existing user: the new chrome-use binary couldn't
find a matching host/extension, silently fell back to raw --remote-debugging-port,
and re-popped the 'Allow remote debugging?' consent dialog.
Keeping com.agent_browser.connect means the renamed chrome-use binary keeps
working with the already-installed host json and the live ab-connect 0.4.2
extension — zero relay break, no dialog, and the store republish becomes an
OPTIONAL cosmetic display-name update (manifest bumped 0.5.0 → 0.4.3, name stays
chrome-use). Only the binary/command name changed for users.
Transferring a logged-in session between Chrome profiles previously needed
an ad-hoc external script to decrypt the source profile's cookie store. Make
it first-class:
- `cookies export --from <profile> [--domain <d>[,<d>]]` decrypts another
profile's on-disk cookies and prints CDP-shaped JSON for `cookies set --curl`.
- `cookies transfer --from <profile> [--domain <d>]` exports + injects into
the connected browser in one shot (reuses the cookies_set path).
Source profile is resolved by directory name, display name, or "auto". The
store is copied to a temp file (immune to a running Chrome's lock/WAL), read
via sqlite3, and values are decrypted (macOS v10: AES-128-CBC, key from the
shared 'Chrome Safe Storage' Keychain entry). httpOnly/secure/per-domain
auth cookies round-trip intact; SameSite=None without Secure is downgraded
so CDP accepts it. macOS only for now (clear error elsewhere).
Release binaries / Build macOS ARM64 (push) Has been cancelled
Release binaries / Build macOS x64 (push) Has been cancelled
Release binaries / Build Linux ARM64 (push) Has been cancelled
Release binaries / Build Linux musl ARM64 (push) Has been cancelled
Release binaries / Build Linux musl x64 (push) Has been cancelled
Release binaries / Build Linux x64 (push) Has been cancelled
Release binaries / Build Windows x64 (push) Has been cancelled
Release binaries / Attach binaries to GitHub Release (push) Has been cancelled
Standalone product rename across the whole repo (issue: project identity):
- Binary/package/repo/skill/docs: agent-browser[-stealth] → chrome-use
(single binary name `chrome-use`; old aliases agent-browser/abs dropped).
- Version: 0.27.0-fork.51 → 1.0.0 (drop the upstream-fork counter).
- Native-messaging host: com.agent_browser.connect → com.leeguoo.chrome_use
(CLI + ab-connect extension in lockstep — this is a breaking handshake change,
extension bumped 0.4.2 → 0.5.0, needs a Web Store republish).
- Config dir: ~/.agent-browser → ~/.chrome-use.
- README/zh: reframed from "stealth fork of agent-browser" to a standalone
product with a small `originally based on vercel-labs/agent-browser` credit.
- Kept AGENT_BROWSER_* env vars working (63 vars across the codebase; renaming
them would break every existing script/skill for no user-facing gain).
Build green, 802 unit tests pass, fmt + clippy clean. Upstream attribution to
vercel-labs/agent-browser preserved.
`open` waits for the `load` event by default. SPAs whose `load` never fires
(a long-pending XHR or a stuck sub-resource holds it open) made `open`
hard-fail after the lifecycle timeout — even though the DOM was ready and
eval/screenshot worked immediately right after.
- Graceful degradation: if the lifecycle event times out but document.readyState
is interactive/complete, navigate returns success carrying a `warning` in the
response (the CLI prints it to stderr; --json keeps the field) instead of
erroring. Only a still-loading document is a real failure.
- `open/goto/navigate` now accept `--wait-until <load|domcontentloaded|
networkidle|none>` so SPAs can return as soon as the DOM is parsed. The URL
parser skips the --wait-until value so it isn't mistaken for the URL.
- WaitUntil::as_str() for the warning label; output.rs surfaces response warnings.
Verified live: --wait-until domcontentloaded returns immediately on a page whose
load never fires; default load on the same page now succeeds at the timeout with
a clear stderr warning instead of failing. Adds parse tests for both arg orders
+ bogus value.
A bare --launch opens an isolated empty profile (no cookies/login/
extensions). A human watching the desktop sees a mystery Chrome window
under an unfamiliar profile and reads it as broken/suspicious.
- Seed the temp profile's Local State (profile.info_cache.Default.name,
the field Chrome's profile chip reads) + Default/Preferences with
'agent-browser (<session>)', so the window self-identifies which agent
session owns it.
- Rewrite the --launch warning to explain it's an isolated test profile and
point at the escape hatches: --profile auto / AGENT_BROWSER_PROFILE=auto
to reuse real Chrome, and --args "--load-extension=<dir>" for extensions.
- SKILL.md documents the same.
Adds a unit test for the profile-label writer.
SKILL.md + click --help + README now cover what agents could otherwise
only discover by trial:
- coordinate click (click <x> <y> / <x>,<y> / --coords) as a first-class form
- tabs / get-text aliases
- the 'stale sessionId … re-open your target URL' relay error and how to recover
- eval/screenshot/network '@ <url>' stamps as a per-read wrong-tab sanity check
- network requests --clear as the 'start capturing fresh' step
handleForwardCdpCommand fell through to anyConnectedTab() when a
daemon-supplied sessionId didn't map to an attached tab, so eval/screenshot/
network silently ran on an arbitrary tab — the root of "eval ran on the
wrong page, no warning" and the blank-screenshot-after-restart symptom.
Now: a provided sessionId/targetId MUST resolve to a real tab or the command
throws an actionable error ("stale sessionId … re-open your target URL").
anyConnectedTab() is only used for genuinely browser-level commands that
specify neither. Manifest 0.4.1 → 0.4.2 (needs a Chrome Web Store republish
for installed users to pick this up).
Field report #8: in extension-relay sessions, reads (eval/screenshot/network)
could silently run against whatever tab drifted into focus, with no signal,
and network capture was intermittently empty.
- #8.1: screenshot and `network requests` now print `screenshot @ <url>` /
`network @ <url>` to stderr (mirrors the existing `eval @ <url>`), and the
responses carry `origin`. A read against the wrong/drifted tab — and the
"0 captured" vs "wrong page" ambiguity — is now obvious.
- #8.3: `network requests --clear` now enables Network capture immediately
instead of lazily on the next read, so requests fired between `--clear` and
the following read are tracked (fixes the "No requests captured" on first
try, works on retry" race). Extracted enable_request_tracking helper.
- #8.2: the daemon version-mismatch restart notice now spells out that
in-memory context (active tab, refs, captured requests) is reset and tells
the user to re-open the target URL if the next read looks blank/wrong.
Verified on a launched browser: coordinate clicks land, screenshot/network
stamps appear, and a fetch after --clear is captured on the first read.
Field-report ergonomics fixes so agents stop wasting a round on a wrong guess:
- Coordinate click is now first-class: `click <x> <y>`, `click <x>,<y>`,
and `click --coords <x>,<y>` dispatch a raw viewport-point click (no
element resolution), reusing the humanize trajectory + press dwell. Was
previously only reachable via eval(elementFromPoint(...).click()).
- Aliases: `tabs` (plural) → the `tab` subcommand tree; `get-text`/`get_text`
→ `get text <selector>`.
- `find <value> <action>` with a bare value (no locator keyword), e.g.
`find "I'm not a robot" click`, now errors with the corrected command
(`find text "I'm not a robot" click`) plus concrete examples, instead of
a bare "Valid options: role, text, ..." list.
Adds parse-layer regression tests for every form.
remove_page_by_target_id left active_target_id dangling when the pinned
page itself was removed, so resolved_active_index silently fell back to
active_page_index — which after a passive about:blank discovery can point
at a blank tab. That matches issue #7's intermittent symptom: `wait` then
eval/snapshot landing on about:blank in a --launch session.
Re-pin to the surviving active page after removing the pinned target so
the pin is never left pointing at a target that no longer exists. Adds
pure regression tests for the re-anchor invariant (BrowserManager needs a
live CDP client, so the method can't be unit-constructed directly).
The srcdoc-iframe contentWindow Proxy returned native window methods
unbound, so iframe.contentWindow.getComputedStyle()/addEventListener()/
setTimeout() ran with the Proxy as `this` and threw "Illegal invocation"
on any page that uses a srcdoc iframe under --launch (FullLaunch). The
sibling matchMedia proxy already bound its methods; this one did not.
Wrap each function in an apply/construct trap that swaps the Proxy
receiver for the real window while passing .prototype/.name/.toString/
identity straight through (a plain .bind() drops .prototype and breaks
instanceof/constructors). Cached in a WeakMap for stable identity.
Verified before/after on a launched stealth browser: getComputedStyle,
addEventListener, setTimeout all OK; .prototype preserved.
From Hermes's fork.49 re-dogfood (9/11 fixes confirmed PASS):
- tab list: a page can set its title to a multi-KB string (= a giant URL); cap
the title column like the URL so the row stays readable.
- skill: clarify that true multi-agent isolation needs the extension-connect path
(per-session tab groups) — raw `--cdp` shares the browser, so a sibling
session's `open` can navigate your tab. Use the extension for concurrent agents.
- skill: prefer `eval --json` for array/object results (plain render is
multi-line / pipe-hostile); note type/fill don't fire keydown (use `keyboard
type` when key events are required).
(Hermes's "find-text click bypasses humanize" was a false alarm — verified both
paths curve; the apparent 1-vs-12 was cursor continuity on the same target.)
Within a session, commands are pinned to the agent's opened tab (fork.47). But
two agents on the same (default) session share one daemon + active tab and
clobber each other. Document that each concurrent agent must use a unique
--session — which gives it its own isolated tab group on the shared real Chrome.
Local stealth verification with no external detector: reports mode (connect vs
launch), live fingerprint probes (navigator.webdriver / window.chrome / plugins /
UA-headless) as pass/fail, and an audit of the active overrides for the path
(incl. the iframe-proxy state from #4). `--json` for a stable shape agents can
gate a sensitive flow on. Distinct from `doctor` (install/env health).
--launch mode scored ~20% stealth on CreepJS because the srcdoc-iframe
contentWindow Proxy trips `hasIframeProxy` — the proxy that hides automation is
itself a fingerprintable tell (violates this fork's own "native > JS lies" rule).
Add a config-driven opt-out (no detectable global): AGENT_BROWSER_DISABLE_IFRAME_PROXY=1
drops the patch via __abStealth.disableIframeProxy → the iframe IIFE early-returns
→ clean 0% CreepJS, trading the niche srcdoc-iframe masking. Default keeps current
behavior. README now documents the --launch 20% honestly and scopes the headline
0% to the extension-connect path. Verified: launch + srcdoc page intact with the
toggle; stealth tests green (config strip-prefix kept in sync).
- snapshot -c (compact) now always keeps lines with an interactive ARIA role
(button/link/textbox/combobox/option/…), not only `ref=`/`": "` lines — so a
clickable control can't vanish from compact output and leave the agent clicking
an empty ref (issue #2 P1). Additive: only ever keeps more. compact tests green.
- stale-ref error now leads with "take a fresh snapshot" and points to the `eval`
fallback for ref-churning SPAs, and demotes AGENT_BROWSER_VERIFY_REF=0 to a
flagged last resort instead of presenting it as the fix (issue #3 P1).
The session's active tab was a bare index into `pages`, which drifts when a
foreign/user/other-session tab is passively discovered, a tab closes, or the list
reorders — so `eval`/`screenshot`/`snapshot`/`click` could land on the wrong page.
With login state that's a safety bug (a fetch firing on the wrong origin), and it
made screenshot disagree with snapshot/eval.
Pin the intended tab by stable target_id (`active_target_id`), set on every
explicit open / tab new / tab switch / connect. `active_session_id` and
`active_target_id` resolve through it (falling back to the index only if the
pinned tab is gone), so all commands stick to the agent's tab regardless of
passive churn — and they all agree.
Verified (--cdp, multi-tab): a window.open foreign tab no longer drifts eval;
tab new / switch re-pin correctly.
Dense pages produced hundreds of legend lines on stdout (Hermes: HN dumped 320).
Print the first 40 with a "… and N more" summary; every marker is still drawn in
the image, and --json still returns the full list.
The biggest manual-cost point in the dogfood reports: `select @ref` is a silent
no-op on non-native dropdowns, and click+wait+Enter on react-select/ARIA/portal
menus took ~20 turns of hand-written eval to get right.
New `pick <selector> --option "<text>"` does it atomically in one in-page async
routine: native <select> → set value + input/change; custom widget → focus +
open (pointer/mouse sequence), poll up to 2.5s for the option to render anywhere
(portals included), match by visible text, scroll it in, fire the full
pointer/mouse sequence. ERRORS loudly if the option never appears — no silent
success.
Verified headless: native <select> → "Gamma"; portal combobox → "欧洲"
(non-ASCII); missing option → explicit error. Documented in the skill.
- eval now prints `eval @ <url>` to stderr (stdout stays the raw value) so an
agent can catch tab drift — e.g. a logged-in fetch that hit the wrong origin —
before trusting the result. Mitigates the issue #2/#3 P0 safety concern. (eval
already returned the origin; the default output just never surfaced it.)
- `type --focused <text>`: type into the currently-focused element with no
selector, for custom widgets that move focus to a hidden input (issue #2 P3).
- AGENT_BROWSER_HUMANIZE set to an unrecognized value now warns once (like the
--humanize flag) instead of being silently ignored (Hermes #3).
A coordinate click resolved from a CSS selector (incl. the getByText/find path's
located node) skipped the occlusion check that @ref clicks already get, so an
overlay on top made the click land on the overlay while still reporting ✓ Done —
the worst failure mode for an agent (Hermes #1, issue #2/#3). Now: if the click
point doesn't hit the target (elementFromPoint isn't the element / a descendant /
an ancestor wrapper), dispatch through the DOM instead, which fires the real
handler. Best-effort probe (a flaky check never blocks the normal path); skipped
for strict CLICK_MODE=coord and non-left/multi-clicks.
Verified: occluded button click hits 0→1 (was silent ✓Done); normal click
unaffected.
Root cause behind Hermes #1 (CLICK_MODE=dom "does nothing") and #2 (--humanize
"does nothing"): both are env vars the daemon reads, but the daemon's env is
frozen at spawn — set them on a command to an already-running daemon and they
were silently ignored. (Confirmed: setting CLICK_MODE=dom at daemon spawn made
dom_click fire; setting it later did not.)
Fix: the client forwards AGENT_BROWSER_CLICK_MODE / AGENT_BROWSER_HUMANIZE in the
command envelope (_clickMode/_humanize); execute_command applies them per command
— mirrors CLICK_MODE into the process env (interaction::click reads it fresh) and
sets the humanize session level. Each command is authoritative.
Verified on an already-running daemon: CLICK_MODE=dom now fires dom_click
(hits 0→1); --humanize human typing applies.
- `eval --file <path>`: read JS from a file, sent verbatim — avoids shell-mangling
of non-ASCII identifiers/strings (Chinese), quotes, and large scripts (issue #3).
- `tab list`: truncate multi-KB URLs (JWT/OTP login links) middle-out with a char
count so the list stays readable (issue #3).
- skill: fix the snapshot example to match real output
(`- role "name" [ref=eN]`, not `@e1 [role]`); document that eval runs in the
page MAIN world with persistent state (top-level `const` collides — use IIFE /
window / unique names) and to prefer --file/--stdin/-b for non-ASCII or big JS.
When connected to the user's real Chrome, mgr.close() disconnected but never
closed the tabs the session opened — so every session (especially one that
failed before calling close, or a forgotten one) left its tabs piling up in the
user's browser. Idle-timeout and shutdown have the same exit path.
Track the target_ids this session creates via Target.createTarget in
`created_targets` (only ever our own tabs — never the user's existing tabs, which
the raw-CDP path attaches to, nor other sessions'). On close(), for the connected
path (not a launched browser, which Browser.close handles wholesale), close each
of those targets — the extension maps Target.closeTarget → chrome.tabs.remove.
Verified against a throwaway --cdp Chrome: open + 2 `tab new` → 3 pages; `close`
→ back to 1 (our 2 closed, the pre-existing tab untouched).
fork.40 errored after 5s ("reload the extension"), which still pushed the problem
onto the user. Extend the relay-reconnect wait to ~15s when the extension is
installed: enough for the MV3 service worker to wake and reconnect on its own
(onStartup after a Chrome restart, or the keepalive alarm). The loop re-checks
the relay file each iteration, so a mid-wait recovery is picked up instantly and
the full window is only spent when the extension is genuinely down. End users no
longer have to do anything when the relay blips.
Root cause of the recurring "Allow remote debugging?" dialog: when the ab-connect
relay was momentarily down (MV3 service worker drops the relay-url file across a
Chrome restart / idle wake), auto_connect_cdp silently fell through to the raw
:9222 DevToolsActivePort path — which pops Chrome 136+'s consent modal, the exact
thing the extension exists to avoid. Even a relay-aware build hit this if it
connected during the blip.
Fix: if the native-messaging host is installed (connect::host_installed() — the
durable signal that the user chose the extension path), auto_connect retries the
relay for ~5s while the SW reconnects, and then ERRORS with an actionable message
instead of attaching to a raw debug port. The raw :9222 path now runs only when
no extension is set up (where the dialog is expected). `--cdp <port>` still forces
the raw path explicitly.