Skip to content

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

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

feat: browserbase-localhost skill — cloud browser that can reach your localhost#109
shubh24 wants to merge 4 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 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-tunnel skill so a Browserbase cloud session can load a local dev server without fully exposing it like a public ngrok URL.

The launch.mjs script wires cloudflared to a 127.0.0.1-only auth proxy that forwards to localhost:<port>. Unauthenticated hits to the public tunnel URL get 401. A per-session UUID is accepted via ?__tunnel, bb_tunnel_auth cookie, 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.md documents launch/cleanup, security expectations, and driving the session via browse + authUrl, Playwright/Stagehand CDP headers, or authUrl alone. README.md lists 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.

…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
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) <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>
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 1 potential issue.

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 a404c80. Configure here.

cf.on("exit", (code) => {
console.error(`[cloudflared] exited with code ${code}`);
if (!shuttingDown) shutdown("cloudflared-exit");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

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

// 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

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?

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


> 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?

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