Recorder always strips credential-bearing query/fragment params (OAuth
codes, tokens, api keys) from URLs before they land in events.jsonl.
The video template takes redact: {"frame.jpg": [{x,y,w,h}]} and
pixelates those page-px regions wherever the frame is drawn, tracking
zooms and after-swaps. Auto-record is back on by default (BH_RECORD=0
opts out); making a video is a separate, discretionary step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recording stays off by default; the user asking ("record this") maps to
start_recording(). Setting BH_RECORD=1 auto-records every session into
session-<timestamp> folders that roll over after BH_RECORD_IDLE seconds
(default 180) of no actions, so unrelated tasks never merge into one
recording. Explicit recordings never roll over.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record a browser session as a folder of per-action screenshots + an
events.jsonl trace (start_recording/stop_recording helpers, marker file
spans CLI invocations, no daemon changes, no new deps). An agent then
edits the recording into a Screen-Studio-style clip via the make-video
skill: a self-contained HTML compositor renders the frames + a
composition on a canvas (realistic Chrome window, calm zoom camera,
synthetic cursor, telemetry overlay, error treatment) and exports webm
through the browser's own MediaRecorder — no ffmpeg.
- src/browser_harness/recorder.py: capture layer (+ 6 unit tests)
- src/browser_harness/{run,helpers}.py: observe() hook + helper exports
- interaction-skills/video-template.html: the compositor/renderer
- interaction-skills/make-video.md: the editor skill
- SKILL.md: recording section
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cross-checked AGENTS.md, README.md, SKILL.md, install.md, and profile-sync.md for accuracy. Tightened the sync_local_profile docstring and the chrome://inspect stderr message to match. Cloud-bootstrap test updated to set BU_AUTOSPAWN. All unit tests pass.
Domain skills auto-injected by goto_url() are community-contributed and quality varies; defaulting them off avoids polluting the average run while preserving the contribution loop. Set BH_DOMAIN_SKILLS=1 to opt in.
* refactor(tests): reorganize into tests/unit and tests/integration
Moves all root-level test_*.py files into a structured tests/ directory:
- tests/unit/ — admin, helpers (was test_screenshot), run
- tests/integration/ — js expression tests
- tests/conftest.py — shared fake_png pytest fixture, eliminating duplication
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: move to src layout, agent-workspace, and fix SKILL.md invocation format
- Move package to src/browser_harness/ and domain-skills/interaction-skills to agent-workspace/
- Fix all browser-harness <<'PY' heredoc examples in SKILL.md and run.py HELP string to use the correct -c '...' flag format (heredoc was never supported by the CLI)
- Update SKILL.md path references from domain-skills/ to agent-workspace/domain-skills/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Markdown-style domain skill for Polymarket scraping via Gamma API
(api-first per repo doctrine) with DOM leaf-div-disambiguation fallback
for CSS-module SPAs. Covers market outcomes, metadata, and comments.
Live-tested against gamma-api.polymarket.com and a live event page:
- 9 outcomes extracted (e.g. April 7: YES 99.95 / NO 0.05, vol $45.7M)
- Metadata: title, end_date, total_volume, category, market_count
- 38 comments fetched (40 raw, 2 deleted skipped)
Gotcha documented: Gamma API comment envelopes for deleted comments
preserve id/createdAt/profile/media/parentCommentID but drop the body
field entirely — naive dict access throws KeyError. Guard with
'if "body" not in c: continue'.
DOM fallback pattern documented (not primary path): Polymarket has
zero data-testid attributes and CSS-module-hashed classes. Leaf-div
disambiguation (children.length === 0 + nearest-common-ancestor
grouping) is the only robust approach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SKILL.md now covers day-to-day usage only. Maintenance commands
(--doctor, --setup, --update) and the architecture section move to
install.md where setup and break-fix content belongs.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- goto() → goto_url()
- click() → click_at_xy()
- screenshot() → capture_screenshot()
These three shared exact names with Playwright but different argument
shapes, causing agent confusion. Updated helpers.py and all markdown
skill files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(run): pass globals to exec so comprehensions resolve free vars
exec(sys.stdin.read()) inside main() passes different dicts for globals
and locals. Python comprehensions and generator expressions compile to a
nested function whose free-variable lookups can only see the globals
dict, so code like
for it in items:
low = it['text'].lower()
hit = any(k in low for k in KEYS) # NameError: 'low'
fails at module level under exec. `low` is stored in the exec locals
dict which the generator's implicit function cannot see.
Passing globals() as the only extra arg makes exec use the same dict
for both globals and locals, and comprehensions resolve cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(skill): eyeball click coords from screenshots; no getBoundingClientRect
The previous Clicking bullet said "screenshot -> look -> click(x,y)" but
did not rule out roundtripping through js("...getBoundingClientRect()")
to compute coords. Agents coming from Playwright / Selenium habits tend
to locate-first-click-second even when the screenshot already shows the
target, which is slower and more brittle (hidden inputs placed at
x=-9999, CSS-transformed elements, pseudo-elements) than just reading
the pixel off the image.
Rewrite the bullet to explicitly suppress that reflex and scope the
DOM-fallback to elements with no visible geometry. Also replace the
loose "compositor level" phrasing with the actual mechanism: Chrome's
browser-process hit-testing, which is why clicks pass through iframes
/ shadow DOM / cross-origin without extra work.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add --version, --doctor, --setup, --update[-y] commands in run.py;
logic lives in admin.py (install-mode detection, GitHub-releases cache
with 24h TTL, dirty-worktree guard, interactive Chrome-attach flow).
- Print a once-per-day startup banner telling agents to run
`browser-harness --update -y` when a newer release is available.
- Rename project to browser-harness in pyproject.toml so PyPI installs
(uv tool install browser-harness) work via the public package name.
- Add .github/workflows/release.yml: on v* tag push, verify the tag
matches pyproject.toml, uv build, and publish to PyPI via trusted
publishing.
- Wire helpers.http_get through fetch_use.fetch_sync(...).text when
BROWSER_USE_API_KEY is set; falls back to the original urllib path
otherwise, preserving the existing str return contract.
- Document the new commands and the agent's self-update duty in
SKILL.md and install.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Fast start snippet used https://browser-use.com purely as an
illustrative "hello world". Pointing it at docs.browser-use.com
gives agents a more useful landing page on their first run.
Refs #102
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* remote: Python API for remote browsers, profiles, and local-profile sync
No CLI, no new entrypoint. Every helper is a Python function callable from
inside a normal `browser-harness <<'PY'` block. run.py pre-imports them.
admin.py:
- start_remote_daemon(name, profileName=None, **create_kwargs)
Now forwards every documented POST /browsers kwarg (profileId, profileName,
proxyCountryCode, timeout, customProxy, browserScreenWidth/Height, ...).
profileName is resolved client-side via list_cloud_profiles — no browser-use
API change needed. Prints liveUrl and auto-opens it locally when a GUI is
detected (macOS/Windows always; Linux needs $DISPLAY / $WAYLAND_DISPLAY);
headless servers print only.
- list_cloud_profiles() — GET /api/v3/profiles + per-profile detail; returns
[{id, name, cookieDomains, lastUsedAt, userId}]. Agents should report
len(cookieDomains) not the full list — profiles can have 500 cookies across
dozens of domains.
- list_local_profiles(), sync_local_profile(name) — shell out to `profile-use`.
sync_local_profile returns the newly-created cloud UUID.
Profile-sync skill rewritten Python-first with the chat-driven flow (ask the
user which profile; summarize by domain count, never dump cookies) and calls
out the two upstream limitations (sync always creates a new cloud profile; no
per-domain filtering) that need a PR to browser-use/profile-use — they can't
be fixed in browser-harness because the Browser Use API has no cookie
upload/download endpoint.
SKILL.md remote-browsers section updated to match, leading with the parallel
sub-agent use case.
* remote: fix misleading 'no GUI' message when webbrowser.open raises
cubic flagged this on #84: if _has_local_gui() is True but webbrowser.open
raises (e.g. no default browser configured), the code fell through to the
final 'no local GUI — share the liveUrl' line, which is wrong on both counts.
Restructure so each branch produces exactly one accurate message.
* 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
* 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>
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>
* 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>