-
Notifications
You must be signed in to change notification settings - Fork 227
feat: browserbase-localhost skill — cloud browser that can reach your localhost #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
66d5502
bc3fc7f
a404c80
90f5657
1095d31
d7a2e8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| --- | ||
| name: browserbase-localhost | ||
| description: Open a Browserbase cloud browser that can reach the user's localhost via an auth-gated cloudflared tunnel. Use when the user wants to run a cloud browser against a local dev server (e.g. localhost:3000), test a local app on a remote browser, or get a shareable Browserbase session link for a local-only URL. Solves the "BB sessions can't see my localhost" gap without exposing the dev server to the public internet via ngrok. | ||
| --- | ||
|
|
||
| # Browserbase for Localhost | ||
|
|
||
| Run a **Browserbase cloud session** that can hit a `localhost` URL on this machine. The cloud browser sees a public `*.trycloudflare.com` URL that is gated by a random per-session secret, so only this BB session can use the tunnel. Random scrapers get `401 Unauthorized`. | ||
|
|
||
| **Use when the user says things like:** | ||
| - "test my localhost:3000 app on a cloud browser" | ||
| - "I want a Browserbase session that can hit my dev server" | ||
| - "give me a shareable BB replay of my local app" | ||
| - "test this on BB but the URL is localhost" | ||
|
|
||
| **Don't use when:** | ||
| - The target URL is already public — use the `browser` skill directly | ||
| - The user wants to use their local Chrome — use `cookie-sync` + local mode | ||
|
|
||
| ## How It Works | ||
|
|
||
| ``` | ||
| BB cloud browser ──HTTPS──► xyz.trycloudflare.com ──HTTP──► local auth proxy (127.0.0.1:auto) | ||
| │ | ||
| │ check: X-Tunnel-Auth header | ||
| ▼ | ||
| user's localhost:<port> | ||
| ``` | ||
|
|
||
| 1. `cloudflared` exposes an ephemeral `*.trycloudflare.com` URL pointed at a local auth proxy | ||
| 2. The auth proxy gates every request on `X-Tunnel-Auth: <random UUID>` | ||
| 3. The launcher creates a Browserbase session and prints the tunnel URL + secret | ||
| 4. You drive the BB session via Playwright/Stagehand, injecting the header via CDP | ||
| 5. On exit, the launcher releases the BB session, kills cloudflared, closes the proxy | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| ```bash | ||
| # One-time install of cloudflared | ||
| brew install cloudflared # macOS | ||
| # or: see https://github.com/cloudflare/cloudflared/releases | ||
|
|
||
| # Env vars | ||
| export BROWSERBASE_API_KEY="..." # from browserbase.com/settings | ||
| export BROWSERBASE_PROJECT_ID="..." | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's remove project ID
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done — |
||
| ``` | ||
|
|
||
| Node.js 18+ required (uses built-in `fetch`). | ||
|
|
||
| ## Step 1 — Launch the tunnel + session | ||
|
|
||
| Run the launcher in the **background**. It prints a single-line JSON config to stdout, then `---READY---`, then stays alive until killed. | ||
|
|
||
| ```bash | ||
| nohup node .claude/skills/browserbase-localhost/scripts/launch.mjs --port 3000 \ | ||
| > /tmp/bb-localhost.log 2>&1 & | ||
| echo $! > /tmp/bb-localhost.pid | ||
|
|
||
| # Wait until the sentinel appears (usually 3-6s) | ||
| until grep -q "^---READY---$" /tmp/bb-localhost.log 2>/dev/null; do sleep 0.5; done | ||
|
|
||
| # Read the JSON config (the line starting with `{`) | ||
| CONFIG_JSON=$(grep -m1 '^{' /tmp/bb-localhost.log) | ||
| echo "$CONFIG_JSON" | jq . | ||
| ``` | ||
|
|
||
| The JSON has these fields: | ||
|
|
||
| | Field | What it is | | ||
| |---|---| | ||
| | `tunnelUrl` | The `https://*.trycloudflare.com` URL the BB browser should hit | | ||
| | `secret` | UUID — must be sent as `X-Tunnel-Auth` on every request | | ||
| | `headerName` | `X-Tunnel-Auth` (the header name to use) | | ||
| | `sessionId` | Browserbase session ID | | ||
| | `connectUrl` | `wss://...` — for `chromium.connectOverCDP()` | | ||
| | `dashboardUrl` | `https://www.browserbase.com/sessions/<id>` — share with the user | | ||
|
|
||
| Always show the user the `dashboardUrl` so they can watch live. | ||
|
|
||
| ### Launcher options | ||
|
|
||
| ``` | ||
| --port <n> (required) local port to expose | ||
| --host <h> (default: 127.0.0.1) local host | ||
| --env prod|dev (default: prod) which BB environment | ||
| ``` | ||
|
|
||
| ## Step 2 — Drive the BB session | ||
|
|
||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is playwright recommended?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( |
||
|
|
||
| ```javascript | ||
| import { chromium } from "playwright-core"; | ||
|
|
||
| const { connectUrl, tunnelUrl, secret } = JSON.parse(configJson); | ||
|
|
||
| const browser = await chromium.connectOverCDP(connectUrl); | ||
| const context = browser.contexts()[0]; | ||
| const page = context.pages()[0] || (await context.newPage()); | ||
|
|
||
| // Inject auth header on every request via CDP | ||
| const client = await context.newCDPSession(page); | ||
| await client.send("Network.enable"); | ||
| await client.send("Network.setExtraHTTPHeaders", { | ||
| headers: { "X-Tunnel-Auth": secret }, | ||
| }); | ||
|
|
||
| await page.goto(tunnelUrl + "/login", { waitUntil: "domcontentloaded" }); | ||
| console.log("Title:", await page.title()); | ||
| await page.screenshot({ path: "/tmp/login.png", fullPage: true }); | ||
|
|
||
| await browser.close(); | ||
| ``` | ||
|
|
||
| ### Option B — Stagehand | ||
|
|
||
| Same idea — inject headers via CDP before any `page.goto()` calls, then navigate to `tunnelUrl` instead of `localhost:<port>`. | ||
|
|
||
| ```javascript | ||
| const stagehand = new Stagehand({ | ||
| env: "BROWSERBASE", | ||
| browserbaseSessionID: sessionId, // reuse the session created by the launcher | ||
| }); | ||
| await stagehand.init(); | ||
| const page = stagehand.page; | ||
|
|
||
| const client = await page.context().newCDPSession(page); | ||
| await client.send("Network.setExtraHTTPHeaders", { | ||
| headers: { "X-Tunnel-Auth": secret }, | ||
| }); | ||
|
|
||
| await page.goto(tunnelUrl); | ||
| await stagehand.act({ action: "click the login button" }); | ||
| ``` | ||
|
|
||
| ### 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: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| ```bash | ||
| SESSION_ID=$(echo "$CONFIG_JSON" | jq -r .sessionId) | ||
| TUNNEL_URL=$(echo "$CONFIG_JSON" | jq -r .tunnelUrl) | ||
| browse --connect "$SESSION_ID" open "$TUNNEL_URL" | ||
| ``` | ||
|
|
||
| but this will fail to load subresources without header injection. Treat as last resort. | ||
|
|
||
| ## Step 3 — Clean up | ||
|
|
||
| ```bash | ||
| # SIGINT the launcher — it ends the BB session, kills cloudflared, closes the proxy | ||
| kill -SIGINT $(cat /tmp/bb-localhost.pid) | ||
| rm -f /tmp/bb-localhost.pid /tmp/bb-localhost.log | ||
| ``` | ||
|
|
||
| Verify the BB session is released: | ||
|
|
||
| ```bash | ||
| curl -s "https://api.browserbase.com/v1/sessions/$SESSION_ID" \ | ||
| -H "x-bb-api-key: $BROWSERBASE_API_KEY" | jq '.status' # → "COMPLETED" | ||
| ``` | ||
|
|
||
| ## Security Model | ||
|
|
||
| What you can tell a security-minded user: | ||
|
|
||
| - The `*.trycloudflare.com` URL exists during the session, **but** every request requires `X-Tunnel-Auth: <random UUID>` — anyone without the secret gets 401 | ||
| - The secret lives in exactly two places: the launcher process on the user's machine, and the headers injected into the BB session via CDP. It is never logged, never sent over the public URL, and never persisted | ||
| - The local proxy strips the auth header before forwarding upstream, so the dev server never sees `X-Tunnel-Auth` | ||
| - The proxy listens only on `127.0.0.1`, never on a public interface | ||
| - Tunnel dies when the launcher exits or the BB session ends | ||
| - Cloudflare-the-company terminates TLS at their edge, so trust includes them. For stricter guarantees (no public URL existing at all), the long-term answer is a native `bb tunnel` with a VPC-internal relay. This skill is the v0. | ||
|
|
||
| ## End-to-End Example | ||
|
|
||
| A complete "test my localhost on a cloud browser, screenshot, share the replay" flow: | ||
|
|
||
| ```bash | ||
| # 1. Launch | ||
| nohup node .claude/skills/browserbase-localhost/scripts/launch.mjs --port 3000 \ | ||
| > /tmp/bb-localhost.log 2>&1 & | ||
| echo $! > /tmp/bb-localhost.pid | ||
| until grep -q "^---READY---$" /tmp/bb-localhost.log 2>/dev/null; do sleep 0.5; done | ||
|
|
||
| CONFIG_JSON=$(grep -m1 '^{' /tmp/bb-localhost.log) | ||
| TUNNEL_URL=$(echo "$CONFIG_JSON" | jq -r .tunnelUrl) | ||
| SECRET=$(echo "$CONFIG_JSON" | jq -r .secret) | ||
| SESSION_ID=$(echo "$CONFIG_JSON" | jq -r .sessionId) | ||
| DASHBOARD_URL=$(echo "$CONFIG_JSON" | jq -r .dashboardUrl) | ||
|
|
||
| echo "Watch live: $DASHBOARD_URL" | ||
|
|
||
| # 2. Drive (your script of choice — Playwright/Stagehand here) | ||
| node -e " | ||
| import('playwright-core').then(async ({ chromium }) => { | ||
| const browser = await chromium.connectOverCDP('$(echo "$CONFIG_JSON" | jq -r .connectUrl)'); | ||
| const context = browser.contexts()[0]; | ||
| const page = context.pages()[0] || await context.newPage(); | ||
| const client = await context.newCDPSession(page); | ||
| await client.send('Network.enable'); | ||
| await client.send('Network.setExtraHTTPHeaders', { headers: { 'X-Tunnel-Auth': '$SECRET' } }); | ||
| await page.goto('$TUNNEL_URL', { waitUntil: 'domcontentloaded' }); | ||
| console.log('Title:', await page.title()); | ||
| await page.screenshot({ path: '/tmp/local-on-bb.png', fullPage: true }); | ||
| await browser.close(); | ||
| }); | ||
| " | ||
|
|
||
| # 3. Clean up | ||
| kill -SIGINT $(cat /tmp/bb-localhost.pid) | ||
| rm -f /tmp/bb-localhost.pid /tmp/bb-localhost.log | ||
|
|
||
| echo "Replay: $DASHBOARD_URL" | ||
| ``` | ||
|
|
||
| ## Common Pitfalls | ||
|
|
||
| | Symptom | Fix | | ||
| |---|---| | ||
| | `cloudflared not found` | `brew install cloudflared` | | ||
| | 401 on every request from BB | You forgot to inject `X-Tunnel-Auth` via CDP. Use `Network.setExtraHTTPHeaders`, **not** `page.setExtraHTTPHeaders` | | ||
| | Root HTML loads but JS/CSS 401 | Same root cause — Playwright's helper only applies to top-level navs. Switch to CDP | | ||
| | Tunnel URL takes 5-10s to be reachable from BB | Normal — cloudflared edge needs to register. Retry once on 502 | | ||
| | Local dev server isn't reached | `curl http://localhost:<port>` first to confirm the dev server is actually up | | ||
| | `BROWSERBASE_API_KEY not set` | `export BROWSERBASE_API_KEY=...` and `BROWSERBASE_PROJECT_ID=...` | | ||
| | WebSockets don't work | The proxy supports HTTP upgrade — make sure your client uses `wss://` (cloudflared quick tunnels are HTTPS-only) | | ||
| | Launcher hangs at "starting quick tunnel" | Network or DNS issue reaching `trycloudflare.com`. `cloudflared tunnel --url http://example.com` to test cloudflared standalone | | ||
|
|
||
| ## When NOT to Use | ||
|
|
||
| - **The URL is already public** (Vercel preview, staging, prod) — use the `browser` skill directly | ||
| - **You need video recording from a local-Chrome session** — that's a different product gap; this skill replaces local with cloud, it doesn't mirror local | ||
| - **Bank/healthcare strict-security customer that disallows any public URL** — even auth-gated. They need a native `bb tunnel` with VPC-internal relay, which doesn't exist yet | ||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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-tunneleverywhere: directory,name:field, title, README entry, and all.claude/skills/...path references.