feat: browserbase-localhost skill — cloud browser that can reach your localhost#109
feat: browserbase-localhost skill — cloud browser that can reach your localhost#109shubh24 wants to merge 4 commits into
Conversation
…uth-gated tunnel Solves the "BB sessions can't see my localhost" gap without exposing the dev server to the public internet via ngrok. Spins up an auth-gated cloudflared quick tunnel paired with a Browserbase session; the cloud browser injects a random per-session secret via CDP on every request, so the public tunnel URL is useless to anyone without the secret. End-to-end tested: 401 without auth, 200 with auth, BB session reaches local dev server through the tunnel, SIGINT cleanly releases everything.
| @@ -0,0 +1,235 @@ | |||
| --- | |||
| name: browserbase-localhost | |||
There was a problem hiding this comment.
let's rename to browser-tunnel everywhere
There was a problem hiding this comment.
Done in bc3fc7f — renamed to browser-tunnel everywhere: directory, name: field, title, README entry, and all .claude/skills/... path references.
|
|
||
| # Env vars | ||
| export BROWSERBASE_API_KEY="..." # from browserbase.com/settings | ||
| export BROWSERBASE_PROJECT_ID="..." |
There was a problem hiding this comment.
Done — BROWSERBASE_PROJECT_ID is no longer required. Heads-up from end-to-end testing (90f5657): I first tried auto-discovering via GET /v1/projects and using the first one, but that lists every project the account can see and the wrong one 401s with "Unauthorized Project ID". The clean fix is to just omit projectId on session create — the API derives it from the (project-scoped) API key — and resolve session.projectId from the response for the release call. Verified it picks the key's correct project. Set the env var only to pin a specific one.
|
|
||
| The crucial bit: you must inject `X-Tunnel-Auth: <secret>` via CDP's `Network.setExtraHTTPHeaders`, **not** Playwright's `page.setExtraHTTPHeaders()`. The latter only covers top-level navigations, so subresources (JS/CSS/API calls) will 401. | ||
|
|
||
| ### Option A — Playwright (recommended) |
There was a problem hiding this comment.
why is playwright recommended?
There was a problem hiding this comment.
Fixed — it shouldn't have been. The real requirement was CDP-level header injection (Network.setExtraHTTPHeaders), not Playwright specifically. But that's now moot: the browse CLI is the primary path (see below), and Playwright/Stagehand are demoted to 'when you need programmatic control'. Dropped the '(recommended)' label.
|
|
||
| ### Option C — `browse` CLI | ||
|
|
||
| The `browse` CLI doesn't support per-request header injection. For browse-CLI flows, **prefer Playwright/Stagehand** (above) which gives you CDP control. If you only need a single navigation, you can connect via: |
There was a problem hiding this comment.
shouldn't this be the main supported approach? do we need to expand feature set to support per-request header injection?
There was a problem hiding this comment.
Agreed, and it's now the main path — no CLI feature needed. The blocker was per-request header injection, so I changed the auth mechanism instead: the proxy now takes the secret as a ?__tunnel=<secret> query param on the first request, plants an HttpOnly cookie, and the browser carries it on every subresource automatically. So browse open --cdp <connectUrl> --session bb <authUrl> just works today. Verified end-to-end against a live BB cloud browser (90f5657): page + CSS + image + fetch() all load through the CLI, cookie is HttpOnly. (Note: https://user:pass@host Basic-auth does not work — Chrome strips URL creds on CDP navigation, which is why it's the cookie approach.)
- rename skill browserbase-localhost → browser-tunnel (dir, name, title, README, paths) - make BROWSERBASE_PROJECT_ID optional — auto-discover first project via /v1/projects - launch.mjs: forward client's WebSocket handshake `head` buffer to upstream (was dropped) - launch.mjs: do local cleanup (cloudflared/proxy) before the release fetch + hard-exit safety timer + time-boxed release, so Ctrl-C never hangs on a slow API - SKILL.md: drop "Playwright (recommended)" framing — explain CDP header injection is the real requirement; Playwright/Stagehand equivalent - SKILL.md: reframe browse CLI as the desired primary path, blocked on a per-request header-injection feature gap (documented as feature request) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… URL The browse CLI has no per-request header injection, so the X-Tunnel-Auth header design forced Playwright/Stagehand. Add Basic-auth support to the proxy so the secret can ride in the URL (https://tunnel:<secret>@host) — the browser replays it on every request, no header injection needed. - proxy accepts the secret as either Basic-auth password OR X-Tunnel-Auth header (authVia/stripAuth), strips whichever credential it consumed - launcher emits authUrl (https://tunnel:<secret>@...) ready for `browse open` - SKILL.md: browse CLI is now Option A (recommended); Playwright/Stagehand demoted to programmatic-control options; security model, e2e example, and pitfalls updated for the dual auth mechanism Auth logic unit-tested (no-auth→401, basic→200+stripped, header→200+stripped, wrong-pw→401). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a404c80. Configure here.
| cf.on("exit", (code) => { | ||
| console.error(`[cloudflared] exited with code ${code}`); | ||
| if (!shuttingDown) shutdown("cloudflared-exit"); | ||
| }); |
There was a problem hiding this comment.
Cloudflared exit during session creation goes undetected
Medium Severity
The cf.on("exit", shutdown) handler is registered at the very end of the setup (after multiple await calls for session creation at lines 214 and 226), but if cloudflared exits during that window, the only active exit listener is the one inside the already-resolved tunnel URL promise (line 207), which calls reject as a no-op. The process continues to create the BB session, emit the JSON config, and print ---READY--- with a dead tunnel URL — and no cleanup or shutdown ever triggers.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit a404c80. Configure here.
…rived project End-to-end testing against a real BB cloud browser revealed two bugs in the previous approach: 1. Basic-auth-in-URL doesn't work: Chrome strips `user:pass@` credentials on CDP navigation, so the BB browser hit the bare URL and got 401. Replaced with query-param → cookie: `?__tunnel=<secret>` authenticates the first request, the proxy plants an HttpOnly `bb_tunnel_auth` cookie, and the browser carries it on every subresource automatically. Verified: page + CSS + image + fetch() all load through `browse open`. 2. Project auto-discovery picked the wrong project: GET /v1/projects lists every project the account sees, and projects[0] 401s with "Unauthorized Project ID". Fixed by omitting projectId entirely — the API derives it from the (project-scoped) API key — and resolving session.projectId for release. proxy now accepts cookie / ?__tunnel / X-Tunnel-Auth / Basic, strips whichever it consumed (plus the query param) before forwarding. SKILL.md updated: browse CLI one-liner uses --session + edge-readiness wait; security model, fields table, e2e example, and pitfalls all reflect the cookie mechanism. Verified e2e: launcher (no PROJECT_ID) → cloudflared → browse open renders full page incl subresources; HttpOnly cookie unreadable by JS; SIGINT releases the session (status COMPLETED) with no hang. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| * node launch.mjs --port 5173 --host 127.0.0.1 --env dev | ||
| * | ||
| * Required env: BROWSERBASE_API_KEY | ||
| * Optional env: BROWSERBASE_PROJECT_ID (defaults to the first project on the account) |
There was a problem hiding this comment.
I think we should just fully remove mentions of project ID no? bc API key is scoped to project anyways
| // BROWSERBASE_PROJECT_ID only to pin a specific project. (Don't guess from | ||
| // GET /v1/projects — that lists every project the account can see, and the | ||
| // wrong one returns "Unauthorized Project ID".) | ||
| let BB_PROJECT_ID = process.env.BROWSERBASE_PROJECT_ID || null; |
There was a problem hiding this comment.
same comment above: would prefer fully removing I think
| headerName: HEADER, | ||
| sessionId: session.id, | ||
| connectUrl: session.connectUrl, | ||
| debugUrl: session.seleniumRemoteUrl || null, |
There was a problem hiding this comment.
should this be selenium here?
| export BROWSERBASE_API_KEY="..." # from browserbase.com/settings | ||
| ``` | ||
|
|
||
| The launcher uses your first Browserbase project automatically. Set `BROWSERBASE_PROJECT_ID` only if you want to pin a specific project. |
There was a problem hiding this comment.
again would remove project ID
|
|
||
| > Playwright can also use `authUrl` directly (`page.goto(authUrl)`) and skip the CDP header — the `X-Tunnel-Auth` route is just the alternative if you'd rather not put creds in the URL. | ||
|
|
||
| ### Option C — Stagehand |
There was a problem hiding this comment.
stagehand should be before playwright no?


Summary
Adds a new skill (
browserbase-localhost) that lets a Browserbase cloud session reach alocalhost:<port>dev server without exposing it publicly.X-Tunnel-Auth: <secret>via CDPNetwork.setExtraHTTPHeaderson every request*.trycloudflare.comURL without the secret gets 401End-to-end test (verified locally)
---READY---in ~4sCOMPLETED, cloudflared exited, proxy closedTest plan
brew install cloudflaredif not already installedpython3 -m http.server 3000)node skills/browserbase-localhost/scripts/launch.mjs --port 3000---READY---appears and dashboard URL is reachablecurl -H "X-Tunnel-Auth: <secret>" <tunnelUrl>/returns local contentcurl <tunnelUrl>/(no header) returns 401Files
skills/browserbase-localhost/SKILL.md— usage docs, security model, Playwright/Stagehand examplesskills/browserbase-localhost/scripts/launch.mjs— zero-dep Node launcher (HTTP+WS auth proxy → cloudflared → BB session, with cleanup)README.md— add to skills tableSecurity model
bb tunnelwith VPC-internal relay. This skill is the v0 wedge.Note
Medium Risk
Introduces a public trycloudflare URL and local proxy that forwards traffic to the dev server; mitigated by per-session secrets and 127.0.0.1 binding, but still security-sensitive for local apps.
Overview
Adds a new
browser-tunnelskill so a Browserbase cloud session can load a local dev server without fully exposing it like a public ngrok URL.The
launch.mjsscript wires cloudflared to a 127.0.0.1-only auth proxy that forwards tolocalhost:<port>. Unauthenticated hits to the public tunnel URL get 401. A per-session UUID is accepted via?__tunnel,bb_tunnel_authcookie,X-Tunnel-Auth, or Basic password; the proxy strips those before the app sees the request and can set an HttpOnly cookie so assets and WebSockets work after the first authed navigation. It creates a BB session, prints JSON plus---READY---, and on SIGINT/SIGTERM tears down cloudflared, the proxy, and requests session release.SKILL.mddocuments launch/cleanup, security expectations, and driving the session viabrowse+authUrl, Playwright/Stagehand CDP headers, orauthUrlalone.README.mdlists the skill in the table.Reviewed by Cursor Bugbot for commit 90f5657. Bugbot is set up for automated code reviews on this repo. Configure here.