April 2026 · Engineering · 6 min read

Why we drive Chrome over CDP instead of spawning a fresh browser

This is an engineering post. If you want the product pitch, it's on the home page. Here I want to explain, with receipts, why Forgeline attaches to your running Chrome over the DevTools Protocol instead of launching a clean Chromium like every other automation stack.

The numbers we were staring at

On our internal benchmark — a synthetic "open a tab, read its DOM, write a summary" task against 30 real websites — the clean-browser path cost roughly:

CDP-attach against an already-open Chrome: ~40ms to establish the WebSocket, zero re-auth, and all 30 sites completed.

How the attach actually works

Chrome, started with --remote-debugging-port=9222, exposes a plain HTTP endpoint at /json/version with a webSocketDebuggerUrl. We hit that, upgrade to WebSocket, and then speak standard CDP:

GET http://127.0.0.1:9222/json/version
→ { "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/<id>" }

// over that socket, a CDP call looks like this:
→ { "id": 1, "method": "Target.getTargets" }
← { "id": 1, "result": { "targetInfos": [ ... tabs you already have open ... ] } }

From there, Target.attachToTarget gives us a session on the tab the user picked, and we drive it through Page, DOM, and Runtime domains. Playwright's connect_over_cdp is what we use under the hood — we just strip out the "launch a new context" default.

The Chrome 136 wall

Starting Chrome 136, --remote-debugging-port is silently ignored if you point it at the default user-data-dir. This is deliberate — a mitigation against cookie-theft malware that was abusing CDP to exfiltrate logged-in sessions. Absolutely the right call from the Chrome team.

Our workaround is a dedicated profile directory (%USERPROFILE%/ChromeAuto on Windows, $HOME/ChromeAuto elsewhere). The installer creates it, launches Chrome once with --user-data-dir pointing at it, and from then on the user signs into whatever services they want the agent to reach. Your real, daily-driver profile is never touched.

What we gave up

Parallelism, honestly. A CDP-attached browser is shared state — two concurrent tasks yanking the active tab in different directions is a race condition waiting to eat someone's afternoon. We lock at the "page" level: one task per tab, and Target.createTarget when we want isolation. It's not as clean as "spin up a fresh context per task," but for one-human-at-a-time workflows it has been fine.

When a fresh browser is the right answer

Scraping at scale. Regression tests. Any workflow where stateless execution is a feature, not a bug. Forgeline isn't trying to be the right tool for those jobs. Playwright and Puppeteer already are.

But when the job is "do the thing I was about to do myself" — Forgeline borrowing your session is the difference between an agent that works and an agent that gets stuck on the login screen.

← Back to blog