6.4 KiB
name, description, allowed-tools
| name | description | allowed-tools |
|---|---|---|
| bu | Direct browser control via CDP. Use when the user wants to automate, scrape, test, or interact with web pages. Connects to the user's already-running Chrome. | Bash, Read, Edit, Write |
bu
Project: /Users/greg/Documents/browser-use/hackathons/bu/
Read helpers.py first. ~260 lines, one tool call. The code is the doc.
Tool call shape
cd /Users/greg/Documents/browser-use/hackathons/bu && uv run run.py <<'PY'
# any python. helpers pre-imported. daemon auto-starts.
PY
run.py calls ensure_daemon() before exec — you never start/stop manually unless you want to.
Setup
uv sync. Enable chrome://inspect/#remote-debugging. Remote browsers: cp .env.example .env + fill BROWSER_USE_API_KEY.
First local daemon start may hang on Chrome's "Allow debugging" dialog. Give ~15s, then retry.
Parallel / remote
BU_NAME picks the daemon (default default). Each name = independent socket /tmp/bu-<NAME>.sock, independent daemon. Remote:
uv run python -c "from helpers import start_remote_daemon; print(start_remote_daemon('work'))"
BU_NAME=work uv run run.py <<'PY' ... PY
uv run python -c "from helpers import kill_daemon; kill_daemon('work')" # stops cloud browser too
Leaving a remote daemon running bills until the session timeout.
Post-task ritual (self-improving harness)
After every browser task, extract ONE generalizable friction point from the interaction (a failed selector strategy, a slow pattern, a missing helper, a confusing result) and make the simplest possible improvement:
- a 2-line helper in
helpers.py, OR - a one-line gotcha in this file, OR
- a correction to a wrong recipe here.
Commit with the task. The skill gets sharper every use. Skip only if nothing was surprising.
What actually works
- Scraping:
js("...custom query..."). Bespoke selectors beat generic DOM helpers. - Clicking:
screenshot()→ look →click(x, y). Passes through iframes/shadow/cross-origin at the compositor level. - Bulk HTTP:
http_get(url)+ThreadPoolExecutor. No browser for static pages (249 Netflix pages in 2.8s). - After goto:
wait_for_load(). - Wrong/stale tab:
ensure_real_tab(). Daemon also auto-recovers from stale sessions on next call. - Iframe sites (Azure blades, Salesforce):
click(x, y)passes through; for DOM usejs(expr, target_id=iframe_target("sandbox")). Iframe rects are iframe-local — add the host iframe's offset for page coords. - Auth wall: redirected to login → stop and ask the user. Don't type credentials from screenshots.
- Raw CDP for anything helpers don't cover:
cdp("Domain.method", **params).
Design constraints
- Coordinate clicks default.
Input.dispatchMouseEventgoes through iframes/shadow/cross-origin at the compositor level. - Connect to the user's running Chrome. Don't launch your own browser.
cdp-useis only forCDPClient.send_raw. Prefer raw CDP strings over typed wrappers.run.pystays tiny. No argparse, subcommands, or extra control layer.- Helpers stay short. No classes, no extra deps beyond stdlib +
cdp-use+websockets. - Don't add a manager layer. No retries framework, session manager, daemon supervisor, config system, or logging framework.
Architecture
Chrome / Browser Use cloud -> CDP WS -> daemon.py -> /tmp/bu-<NAME>.sock -> run.py
- Protocol is one JSON line each way.
- Requests are
{method, params, session_id}for CDP or{meta: ...}for daemon control. - Responses are
{result}/{error}/{events}/{session_id}. BU_NAMEnamespaces socket, pid, and log files.BU_CDP_WSoverrides local Chrome discovery for remote browsers.BU_BROWSER_ID+BROWSER_USE_API_KEYlets the daemon stop a Browser Use cloud browser on shutdown.
Gotchas (field-tested)
- Chrome 144+
chrome://inspect/#remote-debuggingdoes NOT serve/json/version. ReadDevToolsActivePortinstead. - Omnibox popups are fake
pagetargets. Filterchrome://omnibox-popup...and other internals when you need a real tab. - CDP target order != Chrome's visible tab-strip order. Use UI automation when the user means "the first/second tab I can see";
Target.activateTargetonly shows a known target. - Default daemon sessions can go stale.
ensure_real_tab()re-attaches to a real page. - Keep the two
INTERNALtuples in sync.daemon.pyandhelpers.pyeach define one. - Browser Use API is camelCase on the wire.
cdpUrl,proxyCountryCode, etc. - Remote
cdpUrlis HTTPS, not ws. Resolve the websocket URL via/json/version. - Stop cloud browsers with
PATCH /browsers/{id}+{\"action\":\"stop\"}. - React / controlled inputs ignore
el.value=.... Use the native setter to make React see the change:Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(el,v); el.dispatchEvent(new Event('input',{bubbles:true})). - Radio/checkbox via React: prefer
el.click()overel.checked=true— React listens to the click event to drive state. - UI-library buttons (MUI Select, dropdown overlays): JS
.click()on[role=button]often does NOT fire the library's handler. Screenshot →click(x,y)via CDP instead. - Keyboard listeners checking
e.key==='Enter'onkeypress: CDP'scharevent doesn't always fire DOMkeypressfor special keys. Usedispatch_key(selector, 'Enter'). alert()/confirm()block the page thread. Callcapture_dialogs()BEFORE the action, read viadialogs()after.- Same-origin nested iframes don't show up as CDP targets — walk
document.querySelector('iframe').contentDocument(orcontentWindow) recursively. Cross-origin iframes DO appear as targets; useiframe_target("..."). - Shadow DOM:
document.querySelectordoesn't pierce shadow roots. Walk viaelement.shadowRoot.querySelectorAll(and recurse). - Submitting forms: the "Submit" button isn't always the first
button[type=submit]— on React Native Web etc. contact-method buttons share that type. Prefer the button whose text matches/submit/i, fall back toform.requestSubmit(). - Form success signals vary: visible
#success-message, capturedalert()text, console log, or body text change. Check all sources — don't assume one convention.
Interaction notes
interaction-skills/holds reusable UI mechanics such as dialogs, tabs, dropdowns, iframes, and uploads.domain-skills/holds site-specific workflows.