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.
The JSON-array branch of parse_curl_cookies dropped every field except
name/value, so importing a full cookie export (httpOnly session tokens,
per-domain cookies spanning multiple hosts, secure/sameSite/expiry) could
not reconstruct a usable auth state — a single --domain override cannot
cover an export that spans .chatgpt.com, .openai.com, etc.
Pass through url/domain/path/secure/httpOnly/sameSite/expires when present,
accepting common aliases from DevTools / EditThisCookie exports
(http_only, same_site, no_restriction, expirationDate). Bare {name,value}
exports are unchanged. Added a round-trip test.
- README + README.zh: new Anti-detection subsections — "Human-like input
(behavioural stealth)" (curved trajectories / jitter / cadence / eased
scroll-drag, adaptive per-page escalation, off|fast|human) with the
trajectory contrast table, and "Silent operation" (background tabs, no
foreground stealing, focus-emulated). Added AGENT_BROWSER_HUMANIZE to the
tuning-knobs table.
- skill core: agents told operation is silent by default and how/when to use
--humanize (leave on auto; force human for known behavioural targets).
Driving the user's real Chrome should not yank their view around. Now the agent
operates entirely in the background:
- New tabs are created with `background: true` (CreateTargetParams) so opening
one never foregrounds it (the ab-connect extension already used active:false;
this covers the raw-CDP path too).
- Dropped the two AUTO `Page.bringToFront` calls (auto-connect fresh tab, and the
internal active-page switch). The explicit `bringToFront` command is untouched —
surfacing a tab stays opt-in.
- enable_domains now sets `Emulation.setFocusEmulationEnabled(true)` so a
backgrounded agent tab still renders (screenshots work), isn't render-throttled,
and reports document.hasFocus()/visibilityState='visible' — which also removes
the "tab is hidden the whole session" bot tell.
Verified headless: hasFocus=true/visible while backgrounded; click + screenshot
still work. Default behaviour, no flag.
Completes the humanize suite:
- Clicks land on a jittered point inside the element's box (Fast/Human) instead
of its exact centre. `resolve_element_center` now also returns the element
width/height (box_model_dims); the CSS-selector path reports zero size → land
on centre (no jitter, no regression). Jitter is clamped to the inner box so the
click never misses.
- Wheel scrolls split into eased, jittered segments (humanize::scroll_segments,
unit-tested) instead of one instant jump.
- Drag follows the curved trajectory at Fast/Human (linear 10-step at Off).
Off is unchanged throughout. 9/9 unit tests; verified headless — jittered click
still lands (→ iana.org), segmented scroll moves the page.
- Typing: type_text_into_active_context now uses variable, human-like
inter-keystroke gaps from humanize::keystroke_delays when no explicit --delay
is given (Fast/Human); Off stays instant. Explicit --delay still wins.
- CLI: `--humanize off|fast|human` surfaces AGENT_BROWSER_HUMANIZE so the
session's daemon (a child that inherits this env) applies it, overriding the
adaptive detector. Invalid values warn and are ignored.
Verified headless: `--humanize human` + type lands "hello world" correctly.
Deferred: in-bbox landing jitter (helper ready, needs bbox threaded) + wheel/drag
easing.
After each navigation, probe the loaded page for known behavioural anti-bot
vendor fingerprints — cookies (_abck/Akamai, _px/PerimeterX, datadome,
reese84/Imperva, …), script URLs, and window globals — and escalate this
session to HumanizeLevel::Human when one is present, else fall back to the Off
baseline. So ordinary sites run at full speed (instant clicks) and only pages
actually guarded by behavioural detection pay for human-like motion.
`AGENT_BROWSER_HUMANIZE` still forces a fixed level and short-circuits the probe.
Best-effort: a failed probe leaves the level unchanged. Verified end-to-end
(headless --launch): a HUMANIZE=human click on example.com traverses the curved
trajectory and lands correctly (→ iana.org), identical outcome to Off.
Behavioural stealth: a click that teleports the cursor to an element's exact
centre with no approach path and zero press/release delay is a tell that
advanced anti-bot vendors (Akamai/PerimeterX/DataDome) flag, even though our CDP
events are isTrusted.
New `native::humanize` module — pure, unit-tested motion maths (cubic-Bézier
eased trajectories, in-bounds landing jitter, variable keystroke cadence, and an
anti-bot vendor detector) plus a small daemon-wide runtime (current level + last
cursor + per-action seed). `dispatch_click` now moves along a curved,
decelerating path from the last cursor position and dwells before releasing.
Three levels off|fast|human. Default is Off → byte-for-byte the old teleport, so
nothing changes until opted in. `AGENT_BROWSER_HUMANIZE=human` forces it now;
the adaptive per-navigation detector (set_detected_level) and type/wheel/drag
coverage land next. 8/8 unit tests; fmt + clippy clean.
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
Ships the post-fork.34 skill updates into the binary's embedded `skills get core`:
- lead extension setup with the one-click Chrome Web Store install
- nudge agents to file UX feedback at the GitHub issues page
- when an agent hits the "Allow remote debugging?" dialog, self-check the version
and upgrade a stale (<fork.30) build / remove a shadowing npm-pnpm copy
Plus the README opening rewrite (hook-first vs Claude-in-Chrome / web-access /
Playwright) and Chinese README — docs only, ride along.
Other users will hit the same "古董二进制" problem — an agent-browser-stealth
predating the relay-preference (fork.30) ignores the extension relay and pops
Chrome 136+'s "Allow remote debugging?" dialog. So when an agent hits that dialog,
the skill now says to:
1. check `agent-browser --version`; if < 0.27.0-fork.30, upgrade via install.sh
(the npm registry lags — Releases are source of truth), and remove any stale
npm/pnpm copy that shadows it (a tool bundling its own pinned copy needs that
upgraded too);
2. otherwise install the Store extension.
Stops the "keep retrying the raw port" loop.
Add a prominent callout near the top of the core skill asking agents that hit a
rough edge (confusing error, stale @ref, occluded click, flaky wait, missing
feature) to open a quick issue at
https://github.com/leeguooooo/agent-browser-stealth/issues with the command and
expected-vs-actual. Agent-filed friction is the cheapest, highest-signal way to
sharpen the tool (the Hermes dogfood runs already proved this).
The comparison read like a checklist (Runtime.enable leak, permissions footprint
— proof, not pitch). Reframe the opening around the visceral wins:
- headline: "Give your AI agent the browser you already live in"
- pain→relief lead: no fresh Chrome, no re-login, no captcha walls; you watch it
work and take the wheel on 2FA; undetectable because it IS your browser (0% bot)
- three plain "why not just use X?" lines (Playwright/browser-use, Claude in
Chrome, raw debug port) instead of a wall of checkmarks
- the honest feature matrix moves into a collapsible "Full feature comparison".
Mirrored in README.zh.md.