Pairs a remote coding agent with your local browser, so that YOU can login for the agent. Your logged in session is transmitted securely to the agent which can use it on its own browser to access secure websites on your behalf.
When the agent drives a browser and hits a login wall, it signals to YOUR browser requesting the login. You login to the site (using your password manager), and then you approve passing your session state to the agent for use in its browser.
A relay process must be web-addressable from both sides. We are running a relay you can use (all data through the relay is opaquely encrypted) or you can run your own relay.
agent (any browser driver) relay (this repo) human's Chrome
────────────────────────── ───────────────── ──────────────
browser-handoff request ───────────▶ queue ──────WS───────────▶ request window
human logs in, clicks Done
bundle.json ◀── browser-handoff request ◀──HTTPS── queue ◀─HTTP─ session_bundle (sealed box)
import cookies + localStorage, reload, browser-handoff report ──▶ ✓ / ✗ shown in extension
Payloads are end-to-end encrypted (X25519 sealed boxes via tweetnacl); the relay stores opaque envelopes only. Every envelope is Ed25519-signed by its sender and verified by the recipient (spec §5).
Basic steps:
- Install the Chrome extension in your browser.
- Install the Agent skill on the machine where your agent runs (it carries the
browser-handoffCLI). - Pair your agent to the Chrome extension. The agent does this itself the first time it needs a session: it prints a code and asks you to enter it in the extension (Add agent). Entering the code is your one-time approval of that agent — it can't pair itself.
- Use it. When the agent hits a login wall it requests a session; you log in in your own Chrome and click Done. The session goes to the agent; your password never does.
Details for each below.
The extension is prebuilt at dist/browser-handoff-extension.zip
(direct download).
- Download and unzip it into a folder you'll keep (the extension runs from that folder).
- Open
chrome://extensions, turn on Developer mode (top right). - Click Load unpacked and select the unzipped folder.
- Pin the Browser Session Share icon. It defaults to the hosted relay
https://browser-relay.freeflow.im, so no configuration is needed. - Pair it with an agent: get a code from
browser-handoff pair --name <agent>(below), open the extension, Add agent, and enter the code.
(To rebuild the zip after changing the extension: npm run build:ext.)
The browser-auth-handoff skill carries the browser-handoff CLI and the bundle importer, so
installing the skill is the whole agent-side install (no npm install; crypto is vendored).
Quickest, if you have the skill CLI:
npx skill add freeflow-community/browser-session-shareOr, copy this prompt into your coding agent (Claude Code, etc.):
Install the "browser-auth-handoff" skill from https://github.com/freeflow-community/browser-session-share
so you can hand off authenticated browser sessions from a human's Chrome:
1. Clone the repo (git clone https://github.com/freeflow-community/browser-session-share), or git pull if
you already have it.
2. Symlink its skill into your skills directory, e.g.:
ln -s "$PWD/browser-session-share/skills/browser-auth-handoff" ~/.claude/skills/browser-auth-handoff
3. The CLI is embedded at skills/browser-auth-handoff/scripts/handoff/browser-handoff.mjs and runs
on Node with no install. Read the skill's SKILL.md and follow it.
4. Start the shared browser (browser-handoff browser start) and attach over its CDP endpoint; when
you hit an auth wall, run `browser-handoff request --origins <origin> --hint <url> --load`.
Then confirm the skill is installed and summarize how you'll use it.
Or install it manually:
git clone https://github.com/freeflow-community/browser-session-share
ln -s "$PWD/browser-session-share/skills/browser-auth-handoff" ~/.claude/skills/browser-auth-handoffSharing a session is sharing a credential, so a few things keep it safe:
- You log in, not the agent. You type your password in your own Chrome. The agent never sees your password, only the resulting session (cookies + localStorage) for the origins you approve.
- You approve every share. Nothing leaves your browser until you click Done. The request shows which agent is asking, what task, and which sites, and you can Decline. A built-in denylist blocks sensitive origins (identity providers, banking).
- The agent is who it says it is. You pair each agent once by entering its code and confirming its fingerprint. Every message is signed with that agent's key, so a request is provably from your paired agent and can't be forged or impersonated.
- Only your agent can read it. The session is encrypted end-to-end with the agent's public key (X25519 sealed box). Only that agent can decrypt it.
- The relay is untrusted. It just routes sealed, signed envelopes between you and the agent; it never sees your session or your password and can't forge messages.
- You stay in control. Each agent is separate; revoke any one at any time, from the extension or the CLI, and its access ends immediately.
| Path | What |
|---|---|
relay/ |
Single-process relay: pairing registry, per-pairing queues, WebSocket for the extension, long-poll for the CLI, plus a per-pairing CONNECT/forward proxy for tier 2. In-memory with a JSON snapshot (relay/data/state.json). |
extension/ |
Chrome MV3 extension. Registers many agents (an Agents list with add / rename / revoke), one WebSocket per pairing. A request auto-opens a compact window (Start / Done / Decline) labeled with the requesting agent; after Start, a floating panel is injected onto the login tab so Done is right there, surviving the login redirects. Exports cookies (chrome.cookies) and localStorage for the requested origins. For tier 2, applies a scoped PAC so the login egresses through the relay proxy. |
skills/browser-auth-handoff/scripts/handoff/ |
The browser-handoff CLI (embedded in the skill, zero npm deps — vendored crypto): `pair |
skills/browser-auth-handoff/ |
How an agent uses this. Agent-facing skill: recognize an auth wall (agent's own judgment), request a session with the browser-handoff CLI, import the bundle, verify, report. Bundles a reusable Playwright importer (scripts/import-bundle.mjs) and per-driver recipes for Puppeteer / chrome-devtools MCP / CDP (reference/drivers.md). |
testsite/ |
Local toy site with a cookie-auth app and a localStorage-token app. |
test/e2e.mjs |
Automated run of the four MVP success criteria with the real extension loaded in Chromium. |
test/agent-sim.mjs |
The scripted "agent" the e2e suite drives (request → import → verify → report), so the suite exercises the full stack without a real LLM. Not a component anyone runs by hand — a real agent follows the skill instead. |
Requires Node 20+ and Chrome 116+.
By default the CLI and extension use the hosted relay at https://browser-relay.freeflow.im, so
you can skip running your own. Override it with --relay / HANDOFF_RELAY (CLI) or the popup's
Relay field (extension) — for local development, point both at http://127.0.0.1:8787.
npm install
npx playwright install chromium # for the e2e test
# the CLI is embedded in the skill (no install needed); alias it for readability:
alias browser-handoff="node $PWD/skills/browser-auth-handoff/scripts/handoff/browser-handoff.mjs"
# 1. extension: chrome://extensions → Developer mode → Load unpacked → ./extension
# 2. pair (uses the hosted relay by default)
browser-handoff pair --name my-agent
# prints a code like ABCD-EFGH ; enter it in the extension popup
# — or run everything locally —
npm run relay # http://127.0.0.1:8787
browser-handoff pair --name my-agent --relay http://127.0.0.1:8787
# in the popup, set the Relay field to http://127.0.0.1:8787 before entering the codeNow an agent can request a session whenever it hits an auth wall. By hand, against the
local test site (user alice / password wonderland):
npm run testsite # http://127.0.0.1:4321
# ask for a session; a request window pops in Chrome — click Start, log in, click Done
browser-handoff request \
--origins http://127.0.0.1:4321 \
--hint http://127.0.0.1:4321/cookie/app \
--label "Read the cookie app" \
--out ./session.json
# then import ./session.json into your browser context and continue (see the skill)
node skills/browser-auth-handoff/scripts/import-bundle.mjs ./session.json http://127.0.0.1:4321/cookie/app
browser-handoff report --okHANDOFF_RELAY (or --relay) overrides the default relay for browser-handoff pair; the relay URL is
stored with the pairing afterwards, so later commands reuse it.
skills/browser-auth-handoff/ is a Claude Code skill: it tells any agent driving a browser
how to recognize an auth wall (its own judgment, no detector), request a session with the
browser-handoff CLI, import the bundle with scripts/import-bundle.mjs (Playwright) or the
reference/drivers.md recipes (Puppeteer, chrome-devtools MCP, CDP), verify, and report. To
make it available to Claude Code, copy or symlink it into a skills directory:
ln -s "$PWD/skills/browser-auth-handoff" ~/.claude/skills/browser-auth-handoffbrowser-handoff pair --name NAME [--relay URL]
browser-handoff request --origins a,b [--hint URL] [--label TEXT] [--timeout 30m] [--out FILE] [--tier 1|2]
browser-handoff report [--request ID] (--ok | --failed "reason")
browser-handoff proxy --origins a,b
browser-handoff browser <start|status|stop|endpoint> # shared browser (see below)
browser-handoff load --bundle FILE # inject a bundle into the shared browser
browser-handoff status
browser-handoff revoke [PAIRING_ID]
request writes the bundle to --out and prints {"request_id","proxied":false,"silent":false,"out"}
on stdout. Exit codes: 0 bundle, 2 declined, 3 timeout, 4 unpaired. Re-running request with the
same origins while one is pending resumes it (same request_id) instead of raising a second badge.
report defaults to the most recent completed request.
Superset of Playwright storageState (spec §6): cookies[], origins_storage[].localStorage[],
plus origins, env (userAgent, acceptLanguage, timezone, viewport). sessionStorage,
indexedDB, silent renewal, and per-pairing policy are deferred per §13.
Some sites bind a session to the IP it was minted from, so a bundle replayed from the agent's IP is rejected. Tier 2 routes both the human's login and the agent's later traffic through the relay's per-pairing proxy, so both share the relay's egress IP.
browser-handoff request --tier 2 …sends a tier-2 request. On Start, the extension applies a scoped PAC (only the requested hosts go through the proxy) and answers the proxy's auth challenge.- The reply's stdout JSON comes back
{"proxied": true, "proxy": {server, username, password}, …}. The agent builds its browser context with that proxy (the skill's importer takes aproxyoption). - Tier is learned: a failed tier-1 report marks the origins as tier-2 candidates, so a later
requestwithout--tierescalates automatically.browser-handoff proxy --origins a,bprints the proxy settings when those origins are known to need it, elsenull.
Egress stickiness is per pairing; in the single-process relay there is one egress IP, so a real multi-egress deployment is still future work (§12).
Design in multi-agent-spec.md; Phase 1 is implemented.
- Many agents, one browser. The extension registers several agents at once, each a separate pairing to its own agent identity, each with its own WebSocket. The popup's Agents list adds (paste a code), renames, and revokes them individually. Every request is attributed to the agent it came from — verified by that pairing's token and Ed25519 signature, never a self-asserted field — and the badge, window, and in-page panel name that agent.
- One agent, many browsers. Pair each browser (optionally
browser-handoff pair --label NAME).browser-handoff agentslists them;browser-handoff request --pairing <id|label>targets one;browser-handoff use <id|label>sets the default so a plainrequestgoes there (otherwise the newest pairing). - Tier-2 caveat: only one tier-2 login runs at a time per browser, since the proxy auth can't tell which agent a connection belongs to; a second tier-2 Start is refused until the first finishes.
For a long-lived box that runs many agent sessions, don't import a bundle per request. Run one persistent Chrome that agents attach to over CDP; credentials load into its profile once and stay.
browser-handoff browser start # one Chrome, persistent profile, remote debugging
browser-handoff browser endpoint # e.g. http://127.0.0.1:9222Agents attach to that endpoint instead of launching their own browser (Playwright
chromium.connectOverCDP(endpoint), Puppeteer connect, or the chrome-devtools MCP) and open
a tab in the shared profile. When an agent hits a wall, it requests with --load so the
session is injected straight into the shared browser (cookies via CDP Storage.setCookies,
localStorage via a throwaway tab), no per-request import:
browser-handoff request --origins https://app.example.com --hint https://app.example.com/login --loadConfig: BROWSER_HANDOFF_CHROME (path to Chrome/Chromium, if not auto-found), --port
(default 9222), --profile (default ~/.handoff/browser-profile), --headed (default headless).
Caveats: the shared profile is one cookie jar, so concurrent agents share identity per site; one Chrome owns the profile (agents connect, they don't each launch); tier-2 (IP-bound) sessions are not proxied in the shared browser yet.
npm run test:e2e # extension + relay + CLI, end to end (HEADED=1 to watch)
npm run test:shared # shared-browser mode: CDP credential loader (7 checks)The e2e starts a relay and the test site on private ports, launches Chromium with the extension,
pairs through the popup, logs the "human" in, runs the agent sim against both apps, kills a
pending request and re-runs it to prove it resumes, restarts Chromium to prove the pairing
survives, runs a full tier-2 flow (PAC applied on Start, login and agent traffic both
through the relay proxy, proxy cleared on Done, tier learned), verifies signature interop
between the Node and browser crypto, registers a second agent on the same browser and
checks requests are attributed to the right agent and that revoking one leaves the other, and
exercises Decline. 27 checks.
- The extension asks for
http://*/*andhttps://*/*host permissions up front; the spec's per-origin on-demand permissions are a follow-up. - Denylist is hardcoded:
accounts.google.com,login.microsoftonline.com,www.paypal.com. - Cookies are included when their domain equals a requested host or a parent domain of it; the
relay-side queue keeps envelopes for 24h and dedupes by
msg_id. - If no tab for an origin is open when you click Done, the extension briefly opens one in the background to read localStorage, then closes it.
- Relay state survives restarts via
relay/data/state.json(setRELAY_STATE_FILE=to disable).
