Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This plugin includes the following skills (see `skills/` for details):
|-------|-------------|
| [browser](skills/browser/SKILL.md) | Automate web browser interactions via CLI commands — supports remote Browserbase sessions with anti-bot stealth, CAPTCHA solving, and residential proxies |
| [browserbase-cli](skills/browserbase-cli/SKILL.md) | Use the official `bb` CLI for Browserbase Functions and platform API workflows including sessions, projects, contexts, extensions, fetch, and dashboard |
| [browser-tunnel](skills/browser-tunnel/SKILL.md) | Open a Browserbase cloud browser that can reach your `localhost:<port>` via an auth-gated cloudflared tunnel — no ngrok, no public exposure |
| [functions](skills/functions/SKILL.md) | Deploy serverless browser automation to Browserbase cloud using the `bb` CLI |
| [site-debugger](skills/site-debugger/SKILL.md) | Diagnose and fix failing browser automations — analyzes bot detection, selectors, timing, auth, and captchas, then generates a tested site playbook |
| [browser-trace](skills/browser-trace/SKILL.md) | Capture a full DevTools-protocol trace (CDP firehose, screenshots, DOM dumps) alongside any browser automation, then bisect the stream into per-page searchable buckets |
Expand Down
242 changes: 242 additions & 0 deletions skills/browser-tunnel/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
---
name: browser-tunnel
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.
---

# Browser Tunnel — cloud browser → 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: secret (cookie / ?__tunnel / X-Tunnel-Auth)
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 a `<random UUID>` secret, accepted as the `bb_tunnel_auth` cookie, a `?__tunnel=<secret>` query param, or an `X-Tunnel-Auth` header. On the first authed request it plants the cookie, so subresources authenticate automatically
3. The launcher creates a Browserbase session and prints an `authUrl` (`https://...?__tunnel=<secret>`) plus the raw `tunnelUrl` + `secret`
4. You drive the BB session — easiest with the `browse` CLI pointed at `authUrl` (the first navigation sets the cookie; everything after rides it, no header injection needed)
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 var
export BROWSERBASE_API_KEY="..." # from browserbase.com/settings
```

Your API key is scoped to a single project, so no project ID is needed.

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/browser-tunnel/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 |
|---|---|
| `authUrl` | `https://*.trycloudflare.com/?__tunnel=<secret>` — the URL to open. The query param authenticates the first request; the proxy then sets a cookie that covers the rest. **Use this with the `browse` CLI.** |
| `tunnelUrl` | The bare `https://*.trycloudflare.com` URL (no secret) — use when injecting the secret as a header instead |
| `secret` | UUID — the tunnel secret. Carried via the `?__tunnel` query param / `bb_tunnel_auth` cookie, or as `X-Tunnel-Auth` |
| `headerName` | `X-Tunnel-Auth` (header name, for CDP injection) |
| `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 secret can travel two ways. Pick based on your driver:

- **`authUrl` (query param → cookie)** — open `https://host/?__tunnel=<secret>`. The proxy validates the query param on the first request and plants an `HttpOnly` cookie, so the browser then carries the secret on every subsequent request (page *and* subresources) automatically. This is what makes the **`browse` CLI** a clean one-liner. (Don't try `https://user:pass@host` — Chrome strips URL credentials on CDP navigation, so they never arrive.)
- **`X-Tunnel-Auth` header via CDP** — for programmatic drivers (Stagehand/Playwright), inject the header with `Network.setExtraHTTPHeaders`. Don't use a framework helper like `page.setExtraHTTPHeaders()`: it only covers top-level navigations, so subresources will 401.

### Option A — `browse` CLI (recommended)

Attach the `browse` CLI to the session the launcher already created (via its `connectUrl`) and open the `authUrl`. No header injection — the query param authenticates the first request and the cookie covers everything after.

```bash
AUTH_URL=$(echo "$CONFIG_JSON" | jq -r .authUrl)
CONNECT_URL=$(echo "$CONFIG_JSON" | jq -r .connectUrl)

# cloudflared's edge takes a few seconds to register — wait for a 200 first
until [ "$(curl -s -m 5 -o /dev/null -w '%{http_code}' "$AUTH_URL")" = "200" ]; do sleep 2; done

# --cdp pins this named session to the BB browser; follow-ups just use --session
browse open --cdp "$CONNECT_URL" --session bb "$AUTH_URL"
browse snapshot --session bb
browse screenshot --session bb --path /tmp/local-on-bb.png
```

> Use a fresh `--session` name (not the implicit `default`) to avoid "already running in cdp mode" if you have other browse sessions open.

### Option B — Stagehand

Use when you need programmatic control with Stagehand's AI actions. Inject the header via CDP before any `page.goto()`, then navigate to the bare `tunnelUrl` (or just `page.goto(authUrl)` and skip the header entirely).

```javascript
const stagehand = new Stagehand({
env: "BROWSERBASE",
browserbaseSessionID: sessionId, // reuse the session created by the launcher
});
await stagehand.init();
const page = stagehand.page;

// Inject auth header on every request via CDP
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 — Playwright

Same CDP approach as Stagehand — inject the header before navigating, then go to the bare `tunnelUrl`.

```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());

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();
```

> Either driver 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.

## 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 the `<random UUID>` secret (cookie, `?__tunnel` query param, or `X-Tunnel-Auth` header) — anyone without it gets 401
- The secret lives in exactly two places: the launcher process on the user's machine, and the BB session (the `authUrl` it navigates to / the cookie the proxy plants, or the header injected via CDP). It is never logged and never persisted. The cookie is `HttpOnly`, so page JS can't read it
- The local proxy strips whichever credential authed the request (the `?__tunnel` query param, `bb_tunnel_auth` cookie, `X-Tunnel-Auth`, or `Authorization`) before forwarding upstream, so the dev server never sees it
- The secret rides inside the TLS tunnel to Cloudflare's edge; it is never sent in cleartext
- 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/browser-tunnel/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)
AUTH_URL=$(echo "$CONFIG_JSON" | jq -r .authUrl)
CONNECT_URL=$(echo "$CONFIG_JSON" | jq -r .connectUrl)
SESSION_ID=$(echo "$CONFIG_JSON" | jq -r .sessionId)
DASHBOARD_URL=$(echo "$CONFIG_JSON" | jq -r .dashboardUrl)

echo "Watch live: $DASHBOARD_URL"

# 2. Drive with the browse CLI — wait for the edge, attach to the launcher's
# session, open the auth URL (cookie covers subresources), screenshot.
until [ "$(curl -s -m 5 -o /dev/null -w '%{http_code}' "$AUTH_URL")" = "200" ]; do sleep 2; done
browse open --cdp "$CONNECT_URL" --session bb "$AUTH_URL"
browse screenshot --session bb --path /tmp/local-on-bb.png

# 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 | Open `authUrl` (not the bare `tunnelUrl`) so the `?__tunnel` query param sets the auth cookie — or, on a CDP driver, inject `X-Tunnel-Auth` via `Network.setExtraHTTPHeaders` |
| Root HTML loads but JS/CSS 401 | You used a framework helper like `page.setExtraHTTPHeaders()` (top-level navs only) instead of `authUrl` or CDP `Network.setExtraHTTPHeaders` |
| 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=...` (the key is project-scoped — no project ID needed) |
| 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
Loading