Skip to content

feat: browserbase-localhost skill — cloud browser that can reach your localhost#109

Open
shubh24 wants to merge 6 commits into
mainfrom
shubh24/browserbase-localhost
Open

feat: browserbase-localhost skill — cloud browser that can reach your localhost#109
shubh24 wants to merge 6 commits into
mainfrom
shubh24/browserbase-localhost

Conversation

@shubh24
Copy link
Copy Markdown
Contributor

@shubh24 shubh24 commented May 15, 2026

Summary

Adds a new skill (browserbase-localhost) that lets a Browserbase cloud session reach a localhost:<port> dev server without exposing it publicly.

  • Pairs a cloudflared quick tunnel with an auth-gated local proxy (random per-session UUID secret)
  • BB browser injects X-Tunnel-Auth: <secret> via CDP Network.setExtraHTTPHeaders on every request
  • Anyone hitting the *.trycloudflare.com URL without the secret gets 401
  • SIGINT cleanly releases the BB session, kills cloudflared, closes the proxy

End-to-end test (verified locally)

  • ✓ Launcher prints config JSON + ---READY--- in ~4s
  • ✓ Tunnel without auth header → 401
  • ✓ Tunnel with wrong secret → 401
  • ✓ Tunnel with correct secret → 200 + local server HTML
  • ✓ BB cloud browser (via Playwright CDP) navigated to tunnel → request reached local server
  • ✓ SIGINT cleanup → BB session moved to COMPLETED, cloudflared exited, proxy closed

Test plan

  • brew install cloudflared if not already installed
  • Start any local dev server (e.g. python3 -m http.server 3000)
  • node skills/browserbase-localhost/scripts/launch.mjs --port 3000
  • Confirm ---READY--- appears and dashboard URL is reachable
  • curl -H "X-Tunnel-Auth: <secret>" <tunnelUrl>/ returns local content
  • curl <tunnelUrl>/ (no header) returns 401
  • Drive a Playwright/Stagehand script against the BB session — verify replay in dashboard
  • Ctrl-C the launcher — confirm BB session ends and tunnel dies

Files

  • skills/browserbase-localhost/SKILL.md — usage docs, security model, Playwright/Stagehand examples
  • skills/browserbase-localhost/scripts/launch.mjs — zero-dep Node launcher (HTTP+WS auth proxy → cloudflared → BB session, with cleanup)
  • README.md — add to skills table

Security model

  • Public URL exists during session but is auth-gated by a random UUID known only to the launcher and the BB session
  • Secret never logged, never persisted, never sent over the public URL
  • Local proxy strips header before forwarding upstream (dev server never sees it)
  • Proxy binds only to 127.0.0.1
  • Trust still includes Cloudflare (they terminate TLS at edge) — for strict customers, the long-term answer is a native bb tunnel with VPC-internal relay. This skill is the v0 wedge.

Note

Medium Risk
Introduces a public trycloudflare URL gated by a session secret and forwards traffic to local dev servers; misconfiguration or secret leakage could expose localhost, though the design strips secrets upstream and documents trust in Cloudflare TLS termination.

Overview
Adds a new browser-tunnel skill so a Browserbase cloud session can load a dev server on localhost:<port> without opening that server to the public (no ngrok-style exposure of the app itself).

The launch.mjs script wires cloudflared to a 127.0.0.1-only auth proxy that forwards to the local port only when a per-session UUID is present (X-Tunnel-Auth, ?__tunnel=, bb_tunnel_auth cookie, or Basic password for curl). It strips those credentials before hitting the dev server, plants an HttpOnly cookie after the first good request (so assets and WebSockets can auth), creates a BB session, prints JSON + ---READY---, and on SIGINT/SIGTERM/cloudflared exit kills the tunnel, closes the proxy, and REQUEST_RELEASEs the session.

SKILL.md documents launch/cleanup, security expectations, and driving the session via browse (authUrl + connectUrl), Stagehand, or Playwright (CDP Network.setExtraHTTPHeaders when not using authUrl). README.md lists the skill in the table only (no marketplace plugin entry in this diff).

Reviewed by Cursor Bugbot for commit d7a2e8b. Bugbot is set up for automated code reviews on this repo. Configure here.

…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.
@shubh24 shubh24 requested a review from shrey150 May 15, 2026 06:13
Comment thread skills/browser-tunnel/scripts/launch.mjs
Comment thread skills/browser-tunnel/scripts/launch.mjs Outdated
Comment thread skills/browserbase-localhost/SKILL.md Outdated
@@ -0,0 +1,235 @@
---
name: browserbase-localhost
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's rename to browser-tunnel everywhere

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bc3fc7f — renamed to browser-tunnel everywhere: directory, name: field, title, README entry, and all .claude/skills/... path references.

Comment thread skills/browserbase-localhost/SKILL.md Outdated

# Env vars
export BROWSERBASE_API_KEY="..." # from browserbase.com/settings
export BROWSERBASE_PROJECT_ID="..."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's remove project ID

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread skills/browserbase-localhost/SKILL.md Outdated

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is playwright recommended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread skills/browserbase-localhost/SKILL.md Outdated

### 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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be the main supported approach? do we need to expand feature set to support per-request header injection?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.)

shubh24 and others added 2 commits May 28, 2026 15:14
- 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) <[email protected]>
… 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) <[email protected]>
Comment thread skills/browser-tunnel/scripts/launch.mjs Outdated
…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) <[email protected]>
* 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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just fully remove mentions of project ID no? bc API key is scoped to project anyways

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1095d31 — removed BROWSERBASE_PROJECT_ID entirely. The launcher now omits projectId on session create (the project-scoped API key resolves it) and reads session.projectId back from the response purely for the internal release call. No project ID anywhere in the user-facing surface.

// 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment above: would prefer fully removing I think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1095d31 — the env var is gone. Replaced the BB_PROJECT_ID block with an internal projectId derived solely from the create-session response (needed only for the REQUEST_RELEASE body).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up in d7a2e8b — went one further and removed projectId from the release body too. I tested the raw endpoint directly: POST /v1/sessions/{id} with just {"status":"REQUEST_RELEASE"} returns 200 and the session goes COMPLETED — no project ID needed (the SDK's required-projectId type is stricter than the API). So now the project ID is gone from the script entirely, matching the bb sessions update CLI used by browser-trace.

headerName: HEADER,
sessionId: session.id,
connectUrl: session.connectUrl,
debugUrl: session.seleniumRemoteUrl || null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be selenium here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — it wasn't a debug URL, it was session.seleniumRemoteUrl, and it was undocumented/unused. Dropped debugUrl from the output JSON and the header docblock entirely in 1095d31. dashboardUrl already covers watching the session live.

Comment thread skills/browser-tunnel/SKILL.md Outdated
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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again would remove project ID

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in 1095d31 — that line is gone. The Prerequisites now just say the key is project-scoped, so no project ID is needed. Same in the pitfalls table row.

Comment thread skills/browser-tunnel/SKILL.md Outdated

> 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stagehand should be before playwright no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — Stagehand is now Option B and Playwright is Option C in 1095d31, and the inline mention reads "Stagehand/Playwright". Made each self-contained (Stagehand no longer says "same as Playwright").

…vers, harden cleanup

Review feedback (shrey150):
- Remove all BROWSERBASE_PROJECT_ID mentions — the API key is project-scoped,
  so omit projectId on create and read session.projectId back for release.
- Drop the misleading debugUrl (it was session.seleniumRemoteUrl, not a debug
  URL, and was unused/undocumented).
- Lead with Stagehand before Playwright in the driver options.

Cursor bot:
- Register the cloudflared-exit -> shutdown handler before session creation and
  remove the stale tunnel-URL-promise exit listener on resolve, so a cloudflared
  exit during the create window no longer emits a dead tunnel URL.

Verified end-to-end against a live BB cloud browser: launcher ready, 401 without
secret / 200 with (query param + header), HttpOnly cookie planted, secret
stripped upstream, SIGINT -> session COMPLETED + cloudflared/proxy torn down.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1095d31. Configure here.

cf.stdout.on("data", onChunk);
cf.stderr.on("data", onChunk);
cf.on("exit", onExit);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tunnel bootstrap skips cleanup

High Severity

shutdown and the long-lived cf.on("exit") handler are registered only after the tunnel URL promise resolves. If that wait times out or rejects, the top-level await throws before cleanup runs, so cloudflared can keep running and the auth proxy may stay bound with no launcher process managing them.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1095d31. Configure here.

cf.stdout.on("data", onChunk);
cf.stderr.on("data", onChunk);
cf.on("exit", onExit);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cloudflared spawn error

Medium Severity

The tunnel URL promise listens for cloudflared stdout, stderr, and exit, but not the child process error event. If cloudflared is missing from PATH, Node emits error on the spawn handle with no listener, which can crash the process before the promise rejects or shutdown runs.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1095d31. Configure here.

upstreamSocket.pipe(clientSocket).pipe(upstreamSocket);
});
upstream.on("error", () => clientSocket.destroy());
upstream.end();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebSocket upgrade hang

Medium Severity

The auth-gated WebSocket path only handles a successful upstream upgrade (101). If the local dev server responds with an ordinary HTTP status instead of switching protocols, the client socket is never answered and can hang until the user kills the connection.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1095d31. Configure here.

Verified empirically that POST /v1/sessions/{id} accepts
{"status":"REQUEST_RELEASE"} with no projectId (HTTP 200 -> COMPLETED) — the
SDK's required-projectId type is stricter than the API. So the session ID alone
is enough, matching the `bb sessions update` CLI used by browser-trace.

Removes the last internal project-ID reference: no derived variable, no field on
the release body. Re-verified launch -> SIGINT -> session COMPLETED.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants