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.
bug-report.yml: 4 required fields plus a 4-box preflight (searched issues, ran --doctor, read install.md, this is a bug not a question/FR/cloud issue). feature-request.yml: 3 required fields plus a 2-box preflight. config.yml disables blank issues and links to Discussions Q&A and install.md.
Same behavior matrix: HTTPError 404 with a ws_path returns the file's ws URL immediately; 404 without ws_path or any other error keeps polling until the 30s deadline. Removes the flag, the break, the post-loop check, and shrinks the explanatory comment.
Captures durable selectors and gotchas for exporting transcripts from
claude.ai/share/<uuid> URLs:
- [data-testid=user-message] for user turns, .font-claude-response for
assistant turns, [data-testid=page-header] for the chat title
- Container-walk pattern (no semantic wrapper — turns are alternating
siblings of a common ancestor)
- The H2.sr-only "Claude responded:" trap that pollutes innerText if you
select the outer turn div instead of .font-claude-response
- Auth requirement: share pages render the conversation only for
signed-in viewers; logged-out sessions get an empty app shell
Includes a working extract-share-transcript.py that emits both JSON and
LLM-friendly Markdown ({Human|Assistant} headers).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cubic flagged that the original socket.create_connection probe matches any process on 9222/9223, not just Chrome. Mirror daemon.py's fallback by hitting /json/version, so a stale or unrelated listener does not skip the cloud bootstrap.
Drop the three boolean-table tests that mocked every collaborator and re-asserted the literal if-condition. Add a focused test for _local_chrome_listening that covers the false-positive case directly.
On headless servers (VPS, Docker) with no local Chrome, ensure_daemon() fires
before any user script runs and raises immediately — start_remote_daemon() can
never be reached from within a -c script.
Add a pre-check in main(): if no daemon is alive, Chrome is not listening on
known debugging ports (9222/9223), and BROWSER_USE_API_KEY is set, auto-
provision a Browser Use cloud browser before falling through to ensure_daemon().
_local_chrome_listening() probes ports 9222/9223 with a 0.3s timeout rather
than relying on _is_local_chrome_mode(), which only checks for absence of
BU_CDP_WS and would incorrectly trigger cloud bootstrap on a local machine
where Chrome is running but BROWSER_USE_API_KEY is also set (e.g. for profile
sync).
Fixes the behaviour reported in issues #181 and #183.
Tested on a headless Hostinger VPS running hermes-agent in Docker — browser-
harness -c '...' now works without any manual daemon setup when BROWSER_USE_API_KEY
is set.
Chrome 147+ silently disables /json/* HTTP discovery when remote debugging
runs on the default user-data-dir (IsUsingDefaultDataDirectory). The websocket
itself still works and DevToolsActivePort still holds the live ws path, but
get_ws_url() polls /json/version for 30s and dies with a misleading
"DevTools is not live yet" error.
PR #260 deliberately stopped trusting the path stored in DevToolsActivePort
because a stale port file (left behind after switching --user-data-dir on the
same port) had a dead UUID. That fix is correct, but it never falls back to
the file's UUID even when Chrome just wrote the file in the current run.
Read both port and ws path. Try /json/version first (preserves #260's
stale-UUID behavior). On HTTP 404 specifically — which is distinct from
connection-refused/timeout meaning "Chrome still starting" — fall back to
ws://127.0.0.1:{port}{ws_path} from the file. The file's UUID is fresh
because Chrome rewrites it on every launch, and the only persistent reason
/json/version 404s on a live port is the default-profile lockdown. If the WS
upgrade then 404s (true stale UUID), the existing CDP handshake error
surfaces unchanged.
Closes#264.
PR #247 added these under domain-skills/ at the repo root, but the
canonical location for domain skills is agent-workspace/domain-skills/
(referenced in README.md, SKILL.md, and helpers.py).
Chrome leaves DevToolsActivePort behind across runs. If the user previously
launched Chrome with one --user-data-dir on a debug port, then later launches
Chrome with a different --user-data-dir on the same port, the file in the
first profile keeps a stale browser UUID. The TCP probe to that port
succeeds against the new Chrome, but the WS upgrade returns HTTP 404
because the cached UUID is dead -- daemon dies with
'CDP WS handshake failed: HTTP 404'.
Resolve the WS URL via /json/version on the port from DevToolsActivePort
instead of trusting the path stored alongside it. /json/version is
authoritative for whatever Chrome is currently bound to that port, so
stale paths no longer matter. The 9222/9223 fallback below already used
this approach -- now both paths agree.
Reproduced on Windows 11 with one Chrome running on port 9222 with a custom
--user-data-dir while a stale DevToolsActivePort sat in
~/AppData/Local/Google/Chrome/User Data from a prior debug session.
Field-tested 2026-04-29. First batch covering Chinese hotel-booking sites:
- ly-com (同程旅行): hotels.md — prices gated behind login; Nuxt SSR
state holds metadata. Documents the ¥? placeholder pattern, Nuxt data
key randomization, login flow via passport.ly.com.
- ctrip (携程): hotels.md — anonymous price extraction works *only*
with the canonical 6-param URL schema. Documents the schema gate,
homepage-form fallback, phantom-¥ regex trap, no-href-on-cards
reality.
- wehotel (锦江): hotels.md — fully anonymous; covers all Jin Jiang
sub-brands (J酒店/锦江/维也纳/7天/IU etc). Documents the
inconsistent checkinDate vs checkInDate capitalization between list
and detail URLs.
All three include URL patterns, list/detail extraction JS, traps, and
quick-start snippets per the SKILL.md "durable site map" guidance.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #229 (src layout refactor) updated SKILL.md and run.py to the -c form
when the CLI dropped stdin support, but missed install.md (steps 2 and 7)
and interaction-skills/profile-sync.md. Sync them up.
Picks up the still-relevant hunks from #215 (closes#215, refs #213).
Co-authored-by: Andres Gonzalez <62394570+FVTVLIX@users.noreply.github.com>
Rebased from #170 (authored by @mvanhorn) onto current main.
The original branch was based on a stale README; only the two
intentional lines from that PR are reapplied here.
When the browser dies but the daemon process keeps running, target_id and session stay cached on the Bridge. Pre-fix, connection_status swallowed the Target.getTargetInfo failure and returned the cached IDs with page=null, so admin counted the daemon as healthy. Now it returns {error: cdp_disconnected} and admin's existing error-check skips it. Also returns {error: not_attached} when target_id is unset.
Three patterns discovered while populating the Shopify Knowledge Base App
for JING (silent training football brand). Each pattern saved 30+ minutes
of rediscovery; documenting so future agents on any Shopify store benefit.
- README.md: when to use the harness for Shopify admin vs Storefront API
/ Admin API / Shopify CLI; auth notes; Polaris React vs Web Components
detection.
- embedded-apps.md: every Shopify app surfaces in an iframe at
*-app.shopify.prod.shopifyapps.com. Use iframe_target() and pass
target_id to js() calls. Known iframe slugs table (extend as
discovered).
- polaris-inputs.md: Polaris React text inputs reject the standard
React-friendly synthetic value setter pattern — fields fill but Save
button stays disabled. Working pattern is .focus() via JS + CDP-native
keystrokes via type_text(). Includes full add-FAQ implementation +
notes on Polaris's web-component migration (polaris-react archived
Jan 2026).
- knowledge-base.md: canonical automation for the Shopify Knowledge
Base App (Winter '26 Edition). FAQ-pair flow, batch loop pattern,
/pairs/<id> URL signal for save success, brand-voice guidance,
query-log review cadence. App is currently English-only with no
public API — browser automation is the only path until Shopify
exposes endpoints.
Tested with 19 successful FAQ saves on the JING store on 2026-04-29.
Co-authored-by: Claude (JING build agent) <build@playjing.local>
Agent-generated skill covering Big Bang (bigbang.hr) checkout:
Vue/Nuxt form filling quirks, jQuery UI location autocomplete,
GTM dataLayer event patterns, and payment step selectors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When BH_TMP_DIR is set, the caller owns the directory and is expected to
give each daemon its own — so the bu-<NAME> filename prefix is redundant.
Dropping it cuts ~30 chars off the AF_UNIX path on POSIX, which matters
because sun_path is 104 bytes on macOS / 108 on Linux and a long BU_NAME
(e.g. an opencode-style session id) can blow past it.
Concretely the failing path on macOS for a 30-char session id under
~/.local/share/bcode/sessions/<sessionID>/bu-<sessionID>.sock was 117
bytes; with this change it becomes ~83 bytes.
When BH_TMP_DIR is unset, _TMP is the shared default (/tmp on POSIX,
gettempdir() on Windows) and the bu-<NAME> prefix is kept to disambiguate
multiple daemons sharing the dir. Backward compatible by default.
admin._daemon_endpoint_names: when BH_TMP_DIR is set, the dir is per-daemon
by construction, so glob discovery doesn't apply — just check whether our
local endpoint exists. Without BH_TMP_DIR, fall back to the existing
shared-dir glob.
Validation (_check on BU_NAME) still runs in both modes to catch garbage
names early.
Without this, a caller passing BH_TMP_DIR=<custom dir> to a directory that
doesn't exist yet causes the first write (sock/port/pid/log/screenshot) to
fail with FileNotFoundError. Default _TMP (/tmp on POSIX, gettempdir() on
Windows) always exists, so this is latent today; it bites the per-session
scratch-dir use case browsercode is about to introduce.
Single root cause -> single fix: cover screenshots, sock, port, pid, and
log paths uniformly.
DETACHED_PROCESS overrides CREATE_NO_WINDOW per Win32 docs, so combining
them caused Windows to allocate a fresh console for the daemon. Closing
that window killed the daemon and forced Chrome to re-prompt for remote
debugging permission. Drop DETACHED_PROCESS, keep CREATE_NEW_PROCESS_GROUP
for terminal-close survival.
Lets callers (e.g. browsercode per-session scratch) redirect all harness
file output via one env var. Default behavior unchanged when unset:
/tmp on POSIX, gettempdir() on Windows.
helpers.capture_screenshot and the debug-click overlay now route through
ipc._TMP so the same knob covers screenshots.
On Windows, `os.kill(pid, 0)` does not behave like its POSIX
counterpart. Instead of returning silently when the process
exists or raising ProcessLookupError when it doesn't, CPython
on Windows raises:
SystemError: <built-in function kill> returned a result
with an exception set
This happens because the underlying Win32 TerminateProcess API
does not accept signal 0 as an "is alive?" probe — Python's C
implementation hits an internal error path that doesn't set a
proper exception, and the interpreter surfaces SystemError.
`restart_daemon()` already catches `(ProcessLookupError, OSError)`
around both `os.kill(pid, 0)` and `os.kill(pid, signal.SIGTERM)`,
but SystemError isn't a subclass of either, so the harness crashes
on every second invocation on Windows: the daemon's stale pid file
points at a no-longer-running pid, the probe raises SystemError,
and the whole `browser-harness -c '...'` call dies with a stack
trace.
Repro on Windows 11 (Python 3.12, browser-harness 0.1.0):
browser-harness --reload # ok
browser-harness -c 'print(page_info())' # ok
browser-harness -c 'print(page_info())' # CRASH
Fix: add SystemError to the existing except clauses in
restart_daemon(). One-word change in two places, matches the
intent of the existing handlers (treat any "couldn't probe/signal
the pid" failure as "process is gone, move on").