- daemon.py: pass enableFileChooserOpenedEvent=true on Page.enable so
Page.fileChooserOpened actually fires (it's off by default in CDP).
- helpers.py: isinstance guards in page_info() and try/except around the JS
eval in pending_blockers() so CDP-side blockers still return when the page
thread is frozen or the session is gone.
- interaction-skills/permissions.md: tighten the intro (Page.* events do cover
dialogs/file-chooser/downloads — it's permission popups specifically that
have no CDP event), and add Browser.setPermission as the finer-grained
alternative to Browser.grantPermissions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chrome renders permission popups outside the page viewport and emits no CDP
event when they open, so agents needed a full-desktop screencapture (or a
hint from the user) to notice one. This wires up a general detector:
- daemon.py injects a wrapper on every document via
Page.addScriptToEvaluateOnNewDocument that logs calls to geolocation,
notifications, mediaDevices, clipboard, bluetooth/usb/serial/hid, file
pickers, window.print, requestStorageAccess, and <input type=file> clicks
into window.__bu_blockers__.
- The event tap records Page.fileChooserOpened, Page.downloadWillBegin, and
Page.javascriptDialogOpening to a bounded daemon-side deque.
- page_info() surfaces a `blockers` key when either source has entries, so
the agent sees "native popup likely open" without being told.
- pending_blockers() returns the full log (js + cdp) for explicit checks.
- interaction-skills/permissions.md documents detection plus the preferred
pre-empt pattern: Browser.grantPermissions + Emulation.setGeolocationOverride.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* skill: make PATH invocation and new_tab-at-session-start unmissable
Two footguns that agents keep hitting on their first call:
1. They prefix the harness with `cd /path/to/browser-harness && uv run …`
even though `browser-harness` is installed on `$PATH` as a standalone
entrypoint. `uv run` from the wrong cwd actively fails, and the `cd`
bakes a brittle assumption about where the repo lives.
2. They `goto(url)` on the first call, which navigates the user's
currently-active tab and destroys whatever they were doing.
Fast start now:
- uses `browser-harness <<'PY'` (no `uv run`) in the example
- uses `new_tab(url)` instead of `goto(url)`
- adds an explicit two-point callout explaining *why* each rule matters
- mirrors both rules in the "What actually works" bullet list so a
scanning agent sees them even if they skip the intro
- drops the stray `uv run` from the remote-browser snippet
* skill: trim Fast start callout; add read-before-edit note
* skill: require reading full file before using the harness, not just editing
Passing the destination URL to Target.createTarget races with
Target.attachToTarget: by the time the new session is attached and
_mark_tab() runs, the brand-new page is briefly on about:blank with
document.readyState == "complete". A subsequent wait_for_load() then
returns immediately and page_info() reads the about:blank state.
Always create the tab on about:blank, attach, then route real URLs
through the existing goto(). Same contract as goto(url) — the caller
still calls wait_for_load() — so this is consistent with the rest of
the helpers and adds no new event-handling code.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* install: error-driven decision tree, drop unconditional chrome://inspect
The previous bootstrap implied that every attach failure (and any
not-running-Chrome case) needed a chrome://inspect navigation. In
practice the remote-debugging checkbox is per-profile sticky in Chrome,
so for any profile that has ever had it toggled on, just launching
Chrome and polling is enough — chrome://inspect is only needed the
first time per profile, when DevToolsActivePort is genuinely missing.
Restructure step 3 of install.md as an explicit error-keyed decision
tree (no Chrome process / DevToolsActivePort missing / port not live
yet / stale websocket) and add a matching gotcha to SKILL.md. Also fix
a stale `uv run bh` snippet in SKILL.md — the entrypoint is
`browser-harness`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Update install.md
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
* Update install.md
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
The merged tab indicator only marked on Page.loadEventFired,
but Page events weren't enabled for new sessions created by
switch_tab(). Added Page.enable + immediate mark in set_session
handler so the 🟢 appears on every tab switch and survives
goto() navigation.
6/6 tests pass: new_tab, switch_tab, goto, click link,
cmd+click, switch back.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* try WS handshake once, not 12 times
Each retry created a new CDPClient which opened a new WebSocket
connection, triggering Chrome's "Allow debugging" dialog again.
12 retries = 12 stacked popups the user has to dismiss.
Now tries once. If it fails, tells the user to click Allow and
retry — no popup spam.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add tab title indicator for agent-controlled tab
Prepends 🟢 to the page title on switch_tab() so the user can
see which tab the agent controls. Unmarks the previous tab first
so only one tab is marked at a time.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add Microsoft Edge support
Edge is Chromium, so the daemon's CDP bootstrap works unchanged once it
knows where Edge's DevToolsActivePort lives. This adds the macOS, Linux,
and Windows Edge profile paths to the discovery list, plus a one-line
note in install.md clarifying that `chrome://inspect/#remote-debugging`
and the rest of the setup flow apply to Edge too.
* Edge Beta/Dev/Canary, drop Edge from main README
Cover the non-stable Edge channels on macOS, Linux, and Windows so
users on insider builds don't fall through to 'DevToolsActivePort not
found'. Edge SxS (Side-by-Side) is Canary's install dir on Windows.
Keep README's Chrome-only pitch; Edge stays documented in install.md
where setup details belong.
* install.md: move Edge note to bottom, one line
Top-of-section carve-out distracts from the Chrome bootstrap steps.
A one-liner in the cold-start reminders is enough for users who need it.
---------
Co-authored-by: MagMueller <mamagnus00@gmail.com>
Each retry created a new CDPClient which opened a new WebSocket
connection, triggering Chrome's "Allow debugging" dialog again.
12 retries = 12 stacked popups the user has to dismiss.
Now tries once. If it fails, tells the user to click Allow and
retry — no popup spam.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
daemon.py: Page/DOM/Runtime/Network.enable calls now have a 5s
timeout. Previously they could hang indefinitely on heavy pages
(TikTok FYP), preventing the daemon from reaching its socket
listener.
SKILL.md: added one gotcha for when restart_daemon() itself hangs
(kill Chrome entirely and reconnect).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agents rarely know to check for open alert/confirm/prompt/beforeunload
dialogs — they freeze the JS thread, so page_info()'s Runtime.evaluate
returns a stale or misleading viewport dict while the page is blocked.
Track the latest unhandled Page.javascriptDialogOpening in the daemon
(cleared on Page.javascriptDialogClosed) and, when one is open, return
{dialog: {type, message, ...}} from page_info() instead of the usual
viewport dict. The dialogs skill now points agents at this signal.
* Reframe post-task ritual as the default "contribute back" procedure
Renames the section to "Always contribute back" and turns the guidance
into an imperative default. Adds concrete examples of what's worth a PR
(private APIs, framework quirks, stable selectors, URL patterns, waits,
traps), a schema for what a domain skill should capture (the durable
shape of the site, not the run narration), and an explicit do-not-write
list — most importantly banning raw pixel coordinates.
Narrows the scope to `domain-skills/` contributions only; no longer
nudges agents to update `interaction-skills/` or `helpers.py` as part
of this loop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Replace "hesitate" with clearer cost framing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix daemon attaching to invisible omnibox popup on fresh Chrome
When Chrome opens fresh, the only page targets are chrome://
internal pages and the omnibox popup (1px invisible viewport).
The daemon's attach_first_page() fell back to the popup, making
all subsequent work invisible to the user.
Fix: when no real pages exist, create an about:blank tab via
Target.createTarget instead of attaching to the omnibox popup.
Tested configurations:
- Fresh start with no real tabs → creates about:blank (1112x817)
- Navigate without AppleScript → works, tab visible
- Recovery from stale socket → auto-reconnects
- Chrome restart from scratch → creates about:blank
Also adds interaction-skills/connection.md documenting the
omnibox popup problem and startup sequence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add connection skill reference to main SKILL.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CoinMarketCap (internal data-api/v3 fully open, no auth; 25 calls no rate limit),
Quora (full Chrome UA required; push() payloads double-encoded JSON; 3 SSR answers only),
Itch.io (http_get works; game cards via CSS selectors; RSS feeds exist),
Steam (appdetails single appid only; price in cents; ISteamApps/GetAppList dead in 2026),
HowLongToBeat (two-step token flow /api/find/init then POST; comp_* in seconds not hours).
Letterboxd (http_get works on film pages; JSON-LD CDATA gotcha; API needs OAuth),
Gutenberg (Gutendex REST API; text via /cache/epub/; .opf is 404, use .rdf),
Metacritic (internal backend API key in HTML; Nuxt __NUXT_DATA__ not __NEXT_DATA__),
RAWG (API needs key; window.CLIENT_PARAMS in HTML has full game data without key),
OpenLibrary (full free API; missing cover = 43-byte GIF not 404; description dual type).
Glassdoor (Cloudflare managed challenge; browser only; __NEXT_DATA__ + DOM fallbacks),
Medium (?format=json strips XSSI prefix; GraphQL /_/graphql no auth; RSS 10-item cap),
SoundCloud (oEmbed no-auth; __sc_hydration apiClient.id as client_id; API v2 with pagination),
Genius (OS token in UA bypasses 403; internal /api/songs no auth; strip first lyrics div header),
Dev.to (public REST API; burst limit 6 req then 429/1s; listings empty without auth).
Coursera (public API no auth, q=search is POST-only/405 on GET),
DuckDuckGo (Instant Answer API, skip_disambig=1 essential, widget answers unusable),
SEC EDGAR (company UA required for www.sec.gov; 10 req/s; XBRL frames for cross-company),
TrustPilot (http_get works; __NEXT_DATA__ has reviews; 10-page cap per filter).
Eventbrite (JSON-LD ItemList, __NEXT_DATA__ for detail; public API needs auth),
Etsy (DataDome blocks http_get; browser CDP + official API v3 with free key),
CoinGecko (free API, sleep 5s between calls; /coins/list for IDs not symbols),
eBay (http_get works until rate limit; JSON-LD Product schema on detail pages).
Ran actual browser-harness sessions against each site and rewrote
the skill files from live test findings. Key corrections:
GitHub: wait(2) after wait_for_load() for React hydration; search API
separate 10 req/min limit; search/code needs auth (401 unauthed)
HackerNews: athing also matches comment rows (use 'athing submission');
job posts break naive score-zip; html.unescape() required for titles
Amazon: .zg-item-immersion gone from Best Sellers; #priceblock_ourprice
returns null (legacy); review count selector collides with cross-sell
widget — use [aria-label*='ratings'] instead
News: The Verge is Atom not RSS (namespace dict required); Reuters
hard-blocks http_get with 403 even with User-Agent; BBC shows no
consent banner from US IP; parallel fetch is 4.3x faster (0.16s vs 0.70s)
ProductHunt: goto() ERR_ABORTED — always use new_tab(); /posts/ URLs
don't exist (it's /products/); homepage has 30 fixed items no lazy load;
[data-test^='post-item-'] is the correct card selector
Captures three learnings from a real run that mis-reported $3,080 AUD for
a UACC-Rack-12U-Wall (real AU street ~$420-630): Tech Geeks is Shopify so
use /products/<handle>.js for canonical price and SKU; its .js `available`
flag is unreliable so cross-check the DOM for sold-out markers; and
sold-out pages there carry stale/junk prices that must never enter a
final table without a second-source sanity check.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>