Two Chrome windows side by side. The left one is signed in by hand with 2FA cleared, started with --remote-debugging-port=9333. A CDP link joins it to the right, a Playwright script that calls connectOverCDP and reuses browser.contexts()[0], the real session, rather than browser.newContext(), which would be logged out again.

Nearly every browser automation dies in the same place: the login. The scraper, the end-to-end test, the little bot that files a weekly report — the interesting work is all after you are signed in, and getting signed in is where it falls over. A password field is easy. A one-time code texted to a phone is not. Neither is a captcha, a “we don't recognise this device” email, or an SSO redirect that bounces you through three domains and a fingerprint check.

The usual advice is to save the cookies. Log in once, dump the session with Playwright's storageState, and load it on every run. That works right up until the auth is more than cookies — a token held in memory, a session pinned to a device fingerprint, a login that re-challenges when it sees an unfamiliar client. Then the saved state logs in as far as the second wall, and you are back to solving a captcha in a headless browser, which is the one place a captcha is designed to be unsolvable.

So stop trying to reproduce the login. Do it once, as yourself, in a real Chrome window — and then point your script at that window instead of launching its own.

Attach, don't launch

Every Chrome can expose a control channel — the Chrome DevTools Protocol, or CDP — on a local port. Start Chrome with the port open, log in by hand like a person, solve whatever it throws at you, and leave the window sitting there. Then Playwright connects to the browser that is already running rather than starting a fresh one:

# start Chrome yourself, once, with a dedicated profile
google-chrome \
  --remote-debugging-port=9333 \
  --user-data-dir="$HOME/.chrome-automation"
# then log in by hand in that window
import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP('http://127.0.0.1:9333');
const context = browser.contexts()[0];          // the session you logged into
const page = context.pages()[0] ?? await context.newPage();

await page.goto('https://app.example.com/reports');
// you are already signed in — no login step in the script at all

connectOverCDP does not open a browser; it borrows the one you started. The cookies, the tokens, the device the server already trusts — all of it is right there, because it is literally the window you signed into. The captcha and the 2FA happened once, by a human, off the automation's critical path, which is exactly where they belong.

The one line that logs you straight back out: newContext()

This is the mistake that sends people back to the forums swearing CDP is broken. After connecting, muscle memory says browser.newContext() — that is how you start a clean session in normal Playwright. Over CDP it does the opposite of what you want: it opens a new, empty context with no cookies, so you are logged out again in the one browser that was logged in. Use browser.contexts()[0] — the context that is already there. Connecting gives you the existing session; asking for a new one throws it away.

The catch nobody mentions: that port is an open door

A remote-debugging port has no password. Anything that can reach it can drive your browser — read the pages, click as you, walk off with the session you just authenticated. That is not a footnote; it is the whole security model, and it is why you have to be deliberate about two things.

Bind to localhost, and never your everyday profile

Keep the port on 127.0.0.1. Do not bind it to 0.0.0.0 to reach it from another machine unless you have put a tunnel or an allow-list in front of it — an open debugging port on a public interface is a fully authenticated browser handed to whoever finds it. And point --user-data-dir at a throwaway profile, not the Chrome you read your own email in. Recent Chrome enforces the second half of this for you: since version 136 it refuses remote debugging on the default profile precisely so a random web page can't attach to the browser holding your real logins. Take the hint — a dedicated profile is the correct setup, not a workaround.

What it does not fix

Attaching solves the login step, not session lifetime. If the site hands out a token that expires in an hour, it still expires in an hour, and one day your script opens a page that has quietly logged itself out. CDP-attach means that when it happens you walk over, sign in again by hand, and the script picks the session straight back up — no re-plumbing, no headless captcha. It is a way to keep a human in the one loop that genuinely needs one, not a way to remove them. For a fully unattended job against a hostile login, that human step is a feature: it is the difference between a bot that quietly fails and one that pauses for the ten seconds only a person can do.

We built this in on purpose

We make thinbrowser, a browser an AI agent drives through MCP, and agents hit this wall harder than anyone: an agent cannot receive your 2FA text or read a device-verification email, so any login with a second factor is a hard stop. So attaching is a first-class mode. Leave it unset and thinbrowser launches its own clean browser. Set TB_CDP to the debugging port and every tool works against the session you logged into by hand — and when the agent is done, close detaches instead of shutting your window, so your session is still there for the next run.

# you: log in once in a Chrome started with --remote-debugging-port=9333
TB_CDP=9333 npx -y thinbrowser        # the agent drives that signed-in session

The login is the wall most automation never gets over. The move is not to break through it — it is to already be on the other side. That’s the kind of thing we work out at Rebel Studios.