I sync my logged-in sessions from my desktop browser into a headless browser on a VPS, so that automation agents running there stay authenticated. The sync reported success: four cookies injected, matching names and domains. Then the headless browser opened the target site and it said logged-out.
The cookies were in the jar. The page just could not see them.
The actual rule
The headless browser I use is Lightpanda, a Chromium-family build driven over the Chrome DevTools Protocol. Its cookie jar is scoped per CDP connection, not per browser process. Inject cookies over connection A, navigate and read the page over connection B, and B sees an empty jar. Nothing is wrong with the cookies; they simply live in the jar that belongs to A.
This bit me for real on dev.to. My first architecture had every automation process open its own WebSocket to the browser and run its own Network.setCookies followed by navigation. Each process saw its own injected session fine, and every other process saw nothing. Worse, two agents sharing one WebSocket would interleave commands, so a navigate from one could land between another's inject and evaluate.
The fix that stuck
The relay I built now owns the only CDP connection for its whole lifetime. Agents talk to it over plain HTTP on loopback:
-
POST /v1/cdpproxies a CDP command onto that one connection, so every command — inject, navigate, evaluate — executes against the jar that actually holds the sessions. - Processes never touch the WebSocket, so there is nothing to interleave; the relay serializes on its side.
- If the browser restarts, the connection dies and so would the jar. The relay keeps the last synced session in memory and replays it (
Network.setCookiesagain) on the fresh connection as soon as it is back.
That last part matters more than it sounds: a headless browser crashing mid-day should not log out every session it held. Since the jar is per-connection, "persist sessions" really means "re-inject on every new connection", and it is the component that owns connections that has to do it.
The second trap: expires
While fixing this I hit a smaller one worth passing on. Injecting a cookie with an expires attribute was silently discarded — not rejected, just dropped. The call returned success, the cookie never existed. Session cookies without expires went through fine. The workaround is to strip expires on injection and let the runtime jar treat them as session cookies; whatever persistence you need lives in your own snapshot of the sessions, which you re-import anyway for the reason above. Setting an explicit Domain, Secure, and httpOnly with path / also avoided a second class of silent mismatches.
The takeaway
I had been thinking about browser state at the wrong granularity. "The browser has the cookies" is the wrong mental model for CDP-driven browsers — the connection has the cookies, and anything that does not route through the connection that holds them is operating on an empty jar. If your automation stack has more than one component talking to the browser, decide once who owns the single connection and make everyone else go through it. It collapses a whole category of "works in process A, logged-out in process B" bugs, and it gives you one place to do session replay after a crash.
The relay is about 1,200 lines of Python and lives with the rest of the project at Raknaos/lightpanda-session-bridge. If you have ever lost an afternoon to a session that was injected but not visible, this is probably why.
Top comments (0)