TL;DR: When autonomous AI agents need to interact with modern web dashboards, handing them passwords or session tokens in prompts is a security disaster. I built Lightpanda Session Bridge — an open-source MV3 Chrome extension and a hardened loopback relay that safely replicates your live browser session into a local Lightpanda headless runtime via CDP. Zero credentials typed, zero secrets exposed to LLMs.
If you build AI agents that do real work on the modern web, you know the exact wall every developer hits: authentication.
The moment your agent needs to check an AWS billing console, inspect private logs on a SaaS dashboard, or pull data from an internal portal, the demo breaks down. Modern apps don’t live on basic auth; they sit behind Google OAuth, SSO federations, hardware passkeys, and biometric 2FA prompts.
A headless browser cannot tap your security key, answer your phone's authenticator app, or blink at a FaceID prompt.
Faced with this, most builders resort to terrible compromises:
-
Hardcoding passwords into agent prompts or
.envfiles (which leak into LLM context logs, chat histories, and traces). - Copy-pasting session cookies manually into configs (which expire quickly and offer zero scoping or SSRF protection).
- Driving the user’s primary browser via raw CDP (which disrupts real work, risks hijacking other tabs, and introduces scary blast radiuses).
The Lightpanda Session Bridge is built on a different philosophy: keep the authentication ritual with the human, and hand the agent an isolated, authenticated runtime.
+-----------------------------------------------------------------------+
| HUMAN BROWSER (Chrome / Edge / Comet) |
| User logs in via Passkey / Google OAuth / 2FA |
| |
| [ 🐼 Sync Tab ] ---> Extension MV3 extracts strictly scoped cookies |
+---------------------------------------+-------------------------------+
| POST 127.0.0.1:8765
| (with X-Bridge-Token + CORS check)
v
+-----------------------------------------------------------------------+
| LOCAL BRIDGE RELAY (relay/server.py) |
| - Loopback-only (127.0.0.1) |
| - IdP & Private IP blocking (anti-SSRF + DNS cache) |
| - Cookie normalization (__Host-, __Secure-, RFC 6265bis) |
+---------------------------------------+-------------------------------+
| WebSocket CDP Protocol
v
+-----------------------------------------------------------------------+
| HEADLESS RUNTIME (Lightpanda in WSL2 @ :9222) |
| - Isolated V8 / Zig engine |
| - Instant DOM / JS evaluation |
| |
| AI Agent reads data via SDK (lightpanda_client.py) |
+-----------------------------------------------------------------------+
Why Session Transfer Beats Credential Sharing
Passwords and API tokens are the wrong unit of trust for agents. They grant permanent, unrestricted access. Once an LLM agent has your password, you have zero guarantee where that string will travel — subagent handoffs, external telemetry, debug dumps, or third-party inference providers.
A session cookie is fundamentally safer:
- It is ephemeral and expires automatically.
- It can be instantly revoked from your main browser simply by logging out.
- It is strictly scoped to a single target origin.
With the bridge, you authenticate once in your familiar browser. When you click Sync, the extension packages only the cookies relevant to that specific origin and pushes them into an isolated headless browser instance.
The LLM never sees your credentials. The relay never logs a cookie value. The machine gets straight to work.
Architecture: The Three Layers
The architecture is purposely minimal, robust, and audit-friendly:
1. The Chrome Extension (Manifest V3)
Designed with a clean, dark Quota Glass interface. It requires only standard scoped permissions (activeTab, cookies, storage). When clicked, it captures cookies for the active domain, normalizes them, and prepares a transfer envelope.
On first launch, it executes an auto-pairing handshake (/v1/bootstrap) with the local relay, storing a shared cryptographic token in local isolated storage without requiring manual copy-pasting.
2. The Hardened Loopback Relay (relay/server.py)
Listening exclusively on 127.0.0.1:8765, the relay is the security gateway. It:
- Enforces strict origin matching.
- Translates browser cookie structures into Lightpanda-compliant DevTools protocol messages (including converting lowercase
sameSitetags likelaxto Lightpanda's PascalCaseLaxto avoid-31998 InvalidEnumTagCDP crashes). - Normalizes
__Host-and__Secure-cookie prefixes per RFC 6265bis. - Forwards cookies over CDP WebSockets to the headless engine.
3. Lightpanda Headless Engine
Lightpanda is an ultra-fast, open-source headless browser built in Zig with V8, purpose-built for AI automation. Running Lightpanda in WSL2 isolates it from your Windows host environment while keeping execution blindingly fast with tiny memory footprints compared to full Chromium.
The Security Checklist: Defending Against SSRF & Local Leaks
Treating a local HTTP relay as a trusted boundary is how local privilege escalation happens. Because the relay accepts cookies, I designed it as an adversarial SSRF surface from day one:
- 🛡️ Zero Logging: Cookie names and values are never printed to stdout, logged to disk, or saved in history.
- 🔒 Loopback Only: Hardcoded binding to
127.0.0.1. No routable network interfaces exposed. - 🚫 Strict Identity-Provider (IdP) Blacklisting: The relay automatically rejects transfers intended for identity roots —
accounts.google.com,login.microsoftonline.com,appleid.apple.com,github.com, andauth0.comcannot be targeted. - 🛑 SSRF IP & DNS Verification: Target domains must resolve to valid public IPv4/IPv6 addresses. Localhost aliases,
127.0.0.0/8, private subnets (10.0.0.0/8,192.168.0.0/16), and wildcard DNS tools likenip.ioare categorically dropped. DNS lookups are pinned with a 60-second cache to prevent time-of-check to time-of-use (TOCTOU) rebinding. - 🔑 Origin-Restricted Handshake: Web pages or rogue local CLI scripts attempting to query
/v1/bootstrapreceive an immediate403 Forbidden. Only callers presenting a legitimatechrome-extension://Origin header can receive the pairing secret. - 🧪 Live Verified: Backed by 9 automated security test suites, validating private IP rejections, CDP payload structures, and token enforcement.
How AI Agents Interact With The Session
Once the session is synced into Lightpanda, your agent script uses the bundled lightweight Python SDK (lightpanda_client.py):
from lightpanda_client import LightpandaClient
# 1. Connect to Lightpanda CDP runtime
client = LightpandaClient(cdp_ws="ws://127.0.0.1:9222/")
client.connect()
# 2. Attach to or spawn the target page (already carrying the synced session)
client.attach_or_create("https://app.example.com/dashboard")
# 3. Evaluate JavaScript inside the authenticated session context
dashboard_data = client.evaluate("""(() => {
return {
user: document.querySelector('.user-profile')?.textContent?.trim(),
quotaRemaining: document.querySelector('.quota-display')?.textContent?.trim(),
csrfToken: document.querySelector('meta[name="csrf-token"]')?.content
};
})()""")
print(f"Agent operating as: {dashboard_data['user']}")
print(f"Remaining quota: {dashboard_data['quotaRemaining']}")
client.close()
The agent never asked for a password. The user never risked account takeover.
Quickstart (Under 3 Minutes)
1. Clone & Install Dependencies
git clone https://github.com/Raknaos/lightpanda-session-bridge.git
cd lightpanda-session-bridge
pip install -r requirements.txt
2. Launch Lightpanda & The Bridge Relay
In two PowerShell terminals:
./scripts/start-lightpanda.ps1 # Runs Lightpanda CDP on 127.0.0.1:9222 (WSL2)
./scripts/start-relay.ps1 # Starts relay on 127.0.0.1:8765
3. Load the Extension
- Open
chrome://extensionsin Chrome, Comet, or Edge. - Toggle Developer Mode on.
- Click Load unpacked and select the repository's
extension/folder. - Open the popup once while the relay runs — it auto-pairs instantly.
- Navigate to any authenticated site, click the 🐼 icon, and hit Sync Session.
Honest Limitations
- Human-in-the-loop: You must click Sync once per session. This is an intentional security design choice, but it means this is built for supervised agent workflows, not headless server farms starting from scratch.
- Local machine only: The relay strictly refuses remote connections. Your agent script and your browser must reside on the same workstation or dev environment.
- Zig / WSL2 dependency: Lightpanda currently runs most smoothly on Linux/WSL2; the PowerShell scripts manage this automatically for Windows setups.
Try It Out & Contribute
The project is fully open-source under the MIT license:
- 📦 GitHub Repository: Raknaos/lightpanda-session-bridge
- 🌐 Project Landing Page: raknaos.github.io/lightpanda-session-bridge
- 🏷️ Release v0.3.4 (Zip Packaged): GitHub Releases
If you're building autonomous agents that need to navigate authenticated environments safely, take it for a spin and star the repo! Feedback, issues, and PRs are warmly welcome.
Top comments (17)
The main trap I hit with cookie-bridged headless sessions is IdP session revocation cascades. When an identity provider binds session cookies to the TLS fingerprint or client user-agent, replaying those cookies in a headless engine with a different TLS Client Hello can trigger anti-fraud heuristics. On strict platforms, that doesn't just drop a 401 on the agent runner; it revokes the active session on the human's primary browser too.
The other edge case is SPAs that hold short-lived access tokens in memory or Web Workers while only keeping the refresh token in an HttpOnly cookie. If the headless runtime only captures cookies at tab sync time, the agent misses the in-memory state and has to trigger a full page reload to rehydrate the client store before it can call backend APIs.
Both hit real soft spots — thanks for spelling out the failure modes.
On IdP revocation cascades: we haven't tried to make the headless runtime indistinguishable from the browser that authenticated — it's a separate engine, with a different UA and TLS stack — so a strict IdP pinning sessions to a fingerprint will eventually flag the replay, and the cascade you describe (revoking the human's primary session too) is the worst-case outcome. The "treat the runtime as disposable" posture in the README is meant to contain that blast radius, but contain isn't prevent — for strict-IdP origins the honest guidance today is: don't bridge them.
On SPA state: cookie-only sync is a lossy snapshot by design. The extension does capture localStorage alongside cookies and the relay re-injects it, which covers the persisted-token pattern — but tokens held only in memory or in a Web Worker are invisible until a reload rehydrates the client store. That works for most dashboards, yet it's still a hidden dependency that breaks silently the moment an app hydrates from an ephemeral token instead of a cookie. Adding it to the known-limitations list next to the CDP-port one.
Really enjoyed the breakdown of the architecture here. Moving the persistent CDP connection inside the relay daemon and scoping cookies cleanly is definitely the right move.
After digging through the codebase, I noticed two practical edge cases that frequently bite cookie-bridged setups in production(I double checked with AI, so it could actually be right):
Instead of just leaving feedback, I put together a PR addressing both:
Opened PR #1 here if you want to take a look: github.com/Raknaos/lightpanda-sess...
Thanks for actually reading the code before commenting — and opening a PR on top, that's rare.
You're right on the bootstrap point: the current check is literally
request_origin.startswith("chrome-extension://"), so any installed extension can hit the loopback endpoint during pairing and read the token. Pinning the extension ID (TOFU on first bootstrap, enforced after) is the right shape for that fix.One nuance on the cookie side:
chrome.cookies.getAll({ url })returns exactly the cookie set the tab itself would send, so parent-domain cookies landing in the headless runtime is URL-faithful behavior, not over-collection. The real difference is blast radius: a human stays roughly inside one app, while an agent can be navigated (or prompt-injected) into hitting sibling subdomains it was never meant to touch — and those org-wide tokens are valid there too. So I'm with you that host-only scoping is worth having; my worry is the default. Strict host-only breaks SSO flows where the IdP sets its session cookie on the parent domain (login.company.com setting.company.com), which is exactly the enterprise segment the bridge targets. URL-faithful default with host-only as opt-in seems safer to me — what does your PR default to?One skeptical note on the IdP coverage list: no client-side list can prevent revocation cascades — revocation happens IdP-side. The most the bridge can do is detect it fast (401s inside the headless runtime → mark the session stale in the relay). Do your tests cover that detection path, or only the scoping/origin logic?
Appreciate this detailed reply.
To answer your two questions:
Left a follow up on the GitHub PR regarding rebasing the TOFU extension pinning. Looking forward to seeing v0.4.x roll out.
Taking your offer: please go ahead and rebase the TOFU pinning (with its tests) onto current main and keep this PR — we'd rather merge it from you than reimplement, and it keeps you as the originator in git history.
Heads-up on what moved under the branch since you opened it: v0.4.2 shipped yesterday. The relay now keeps the single persistent CDP connection plus the /v1/cdp proxy, and 0.4.1 added /v1/sessions and /v1/sessions/clear. Your pinning check plugs in at the same choke point as the existing _check_extension_caller / _require_extension_origin pair in relay/server.py — those currently accept any chrome-extension:// origin (or none, for the CLI path), which is exactly the hole you're closing, so the natural shape is a first-run pinned-ID comparison layered on top rather than a parallel mechanism. The two new endpoints just need to fall under the same gate, which should be a small diff once rebased.
On the fast-401 detection idea — still agreeing with you, it belongs after the pinning lands. The pin is the cheap boundary; detecting server-side revocation inside the relay is a different layer worth doing properly.
Thanks for staying on this — the rebase conflicts should be mostly limited to the routing table and the tests, and I'll review same-day once it's pushed.
Done! Just rebased the branch onto current main (v0.4.2) and updated PR #1.
Kept it strictly scoped to the TOFU extension ID pinning layered onto
_check_extension_caller/_require_extension_originand covering/v1/sessions,/v1/sessions/clear, and/v1/cdp. All 10 security tests are passing cleanly with zero merge conflicts. Ready whenever you get a chance to review.Nice, thanks for the clean rebase and for keeping the scope tight this time — the diff is now exactly the TOFU extension-ID pinning layered onto
_check_extension_caller/_require_extension_origin, with/v1/sessions,/v1/sessions/clearand/v1/cdpall gated consistently, and the CORS header correctly demoted from "any chrome-extension://" to "only a valid pinned one". The octal IPv4 rejection you slipped into_is_global_hostname(leading-zero forms like0177.0.0.1) is a genuinely good catch too — that SSRF variant was easy to miss and it's a real bypass risk against a naive parser, so I'm glad it's in here.One design point I want to think through with you before merge, because it's the part I'm least sure about: as it stands the relay ships no baked-in default pin, so
_require_extension_originauto-pins whichever caller hits/v1/bootstrapfirst andis_valid_extension_originreturns True for any extension until then. That makes the guarantee "first caller wins". On a normal desktop that's the real popup, fine — but if the extension is installed and the user hasn't opened the popup yet, a malicious extension that races/v1/bootstrapat browser start could pin itself before we do, and from then on it's the trusted origin for the secret-delivering path. Do we want that, or should we ship the official ID as a hard default pin so auto-pin only ever adds dev IDs viaLP_BRIDGE_ALLOWED_EXTENSION_IDSrather than establishing trust from scratch? My instinct is the latter — the pin's whole value is that it can't be claimed by whoever gets there first.Tests read well, 10 passing is reassuring and the pin/unpin coverage is the right shape. Want me to take a pass at a default-pin tweak on top of your branch, or would you rather fold it in yourself so the PR stays a single coherent story? Either works for me.
You're 100% right on the startup race. Letting an arbitrary first caller claim the pin before the user ever touches the popup leaves a window for a rogue extension to hijack pairing.
Folded it right into the PR so it stays a single clean commit:
fcigkjkchglchhohedljlenopbkgnino) as the default pin fallback, completely eliminating the browser startup race.LP_BRIDGE_ALLOWED_EXTENSION_IDSor setLP_BRIDGE_TOFU=1if they want open first-caller pinning.Amended and force-pushed to PR #1: github.com/Raknaos/lightpanda-sess...
The relay has a token and an origin check, but the thing it produces — an authenticated Lightpanda on a CDP port — has no equivalent, and that is where the cookies end up living. I checked this on a throwaway Chrome on its own port a few minutes ago:
GET /jsonanswers 200 with no header at all and hands back awebSocketDebuggerUrlfor every target, 7 of them. Then I opened a page with one client, setsess=secret-abcon it, and a second, completely independent process holding nothing but the port number read the URL and the cookie straight back.So the credential never enters a prompt, which is the part you fixed, but the session it produces is drivable by anything on the box that can open a loopback socket — and on an agent host that population includes the agent's own subprocesses and whatever it was told to run. Loopback is a network boundary, not a process one.
If it helps, the cheap discriminator is that
9222accepting an unauthenticated/jsonis the same posture as compromise 3 in your list, just pointed at a browser you consider disposable rather than the human's. Worth saying out loud in the README next to the relay token, since that token is what makes readers assume the whole path is authenticated.You're right on every point, and I like the "cheap discriminator" framing. I verified the same behavior on our side:
GET /jsonon 9222 answers 200 with no headers, and a second process holding only the port number can read back cookies set by the first. The relay authenticates its own API, but the runtime it produces has no equivalent — and the currentlightpanda serve(checked--helpon our nightly) exposes no unix-socket or auth option for the CDP listener, so there is no clean fix at our layer today.Since the docs were exactly where the false sense of security came from, I've added a Known Limitation entry to the README's security guarantees stating precisely what you described — loopback is a network boundary, not a process one, and on an agent host that population includes the agent's own subprocesses (commit 69b76e8). The operating posture until the real fix lands: treat the synchronized runtime as disposable, sync only origins you'd be comfortable exposing to local processes, keep the port loopback-bound, shut the browser down between runs.
The genuine fix is probably upstream — an authenticated or socket-based CDP listener. Worth an issue on lightpanda-io/browser; if you open one with your repro, I'll confirm with ours and reference it from the README.
The important distinction here is keeping authentication outside the agent's context boundary. Too many agent workflows still treat credentials as just another input, when session handling, scope, and isolation are really architecture problems. We've seen similar patterns matter in agent systems at IT Path Solutions giving the agent only the access mechanism it needs, without making secrets part of its reasoning context, significantly reduces the blast radius. The explicit human Sync step is also a sensible trade-off for supervised workflows.
Thanks — and you've put your finger on exactly why we resisted the "just put the cookie in an env var" pattern that most agent tutorials reach for. A session envelope that the agent consumes at runtime is a fundamentally different object from a secret sitting in its reasoning context: it's scoped to one origin, it expires, and revoking it doesn't require rotating anything the agent ever saw.
The deliberate Sync click turned out to be valuable beyond the security story, too — it makes session lifetime visible and human-paced, which matches how supervised agent workflows actually run. If we ever add long-lived server-side sessions, that trade-off will need rethinking; the current design honestly only works because the human stays in the loop.
Your blast-radius framing also matches what a commenter above found in practice: the synchronized runtime itself is still the softest part of the chain, so "give the agent only the access mechanism it needs" has to include killing that runtime when the job is done.
The loopback relay and explicit origin checks feel like the right boundary. I’d also show the origin and expiry next to each transferred session, since a stale cookie is easy to mistake for a current login while debugging.
Thanks a lot for the feedback! You're totally right — silent cookie expiration is one of the most frustrating things to debug when an agent suddenly hits a 401.
Showing the shortest TTL / earliest expiry date alongside the origin in the popup (and in the client SDK response) is a great quality-of-life improvement without leaking any actual cookie values.
Putting this on the roadmap for the next minor release!
Thanks for this article, a valuable read both for us and for our community! Celine, from the Lightpanda team
Thanks Celine — glad it resonated with the team! This came straight out of running the bridge in production, so having Lightpanda folks read it and pass it along to the community genuinely means a lot. And if anyone on the team has opinions on where session sync should go next — expiry/TTL visibility in the popup, extension-ID pinning during pairing, multi-profile sync — we'd love that input. You see far more real-world usage patterns across the ecosystem than we do from one deployment.