What changed in Chrome 136

Starting with Chrome 136, the --remote-debugging-port and --remote-debugging-pipe switches are ignored when Chrome runs on its default user data directory. They only work together with --user-data-dir pointing at a non-standard directory. Google's stated reason is cookie theft: after App-Bound Encryption shipped on Windows, attackers increasingly used remote debugging to pull cookies out of a running, logged-in Chrome. A non-default data directory gets different encryption keys, so a debug session on it cannot reach the cookies of your real profile. The announcement is on the Chrome for Developers blog; the two workarounds it offers are a custom user data directory for debugging, and Chrome for Testing for automation.

For a developer who wanted a debugger, that is fine. For anyone who wanted an agent to work inside the browser they are signed into, both workarounds are the same answer: not that browser.

Why the sanctioned workarounds do not solve the problem

A separate --user-data-dir gives you a port, but it is a fresh profile. Every site is logged out. The agent's first task on GitHub, Google Cloud or Stripe is now "log in", and for anything with a passkey, a hardware key or a phone prompt, that is where the task ends. Copying your real profile directory into the new one does not restore the sessions either: per the same post, the non-default directory is encrypted with different keys, and that is the whole point of the change. In our own testing before we moved off the port, every run that fell back to a separate profile hit a login wall on the first site it opened.

Chrome for Testing keeps the old behaviour, but it is a separate binary with its own, empty profile. Same wall.

There is one more constraint that rules out the obvious hack of "just launch a second Chrome on my real profile with a port". A Chrome profile can be opened by one process at a time (the SingletonLock in the profile directory). If Chrome is already running, your second launch just hands the request to the first one, and the flags are dropped.

The route that still reaches your real profile

What is left is the route Chrome itself sanctions for extensions: the chrome.debugger API. An extension installed in your normal Chrome can attach to a tab and send Chrome DevTools Protocol commands to it, in the profile you are already signed into, with no port and no second browser. To get commands from a program on your machine into that extension, you use native messaging: Chrome launches a host process you register once, and the two talk over stdin and stdout.

This is not an exotic design. Anthropic's Claude in Chrome connects to Claude Code the same way, for the same reason. The pieces are:

1. A Manifest V3 extension with the debugger, nativeMessaging and tabs permissions. Its service worker receives a command, calls chrome.debugger.sendCommand on the target tab, and sends the result back.

2. A native messaging host: a manifest JSON file registered in the registry (Windows) or a well-known directory (macOS and Linux) that names the executable Chrome should spawn. The protocol is length-prefixed JSON on stdin/stdout.

3. Your CLI or agent, which talks to the host over a local pipe or socket. The host multiplexes several clients onto the one extension connection.

What we measured when we built it

Computron switched from the debug port to this bridge in September 2026. These are the measurements from that work, taken on Windows 11 with Chrome 153 (details and dates are in the project's design notes on GitHub).

A .bat works as the host executable on Windows. The documentation only mentions .exe, but Chrome launches the batch file with argv = ["chrome-extension://<id>/", "--parent-window=0"], so a one-line wrapper around node host.js is enough. One caveat we hit later: when Windows Terminal is the default terminal, a .bat host can briefly show a console window, because Chrome starts it through cmd.exe. A real executable avoids that.

An open native port keeps the service worker alive. Manifest V3 service workers are normally killed after about 30 seconds of inactivity, which is why many extension bridges ship a heartbeat. With a native messaging port open, our worker survived more than 4.7 minutes with no traffic at all, with no heartbeat.

Per-command overhead is small. Bridge versus port, same page, same commands: connecting from the CLI to the pipe 2 ms, bridge.ping 1.6 ms, DOM.getDocument 2.8 ms against 0.8 ms, Runtime.evaluate 2.5 ms against 1.0 ms, Input.dispatchMouseEvent 16.7 ms against 16.5 ms. A two-page scenario ran in 1,759 ms over the bridge and 1,565 ms over the port, three out of three successes on both.

The one big cost is page observation, and it is not the transport. Accessibility.getFullAXTree on a 2,151-node page took 282 ms over the bridge and 125 ms over the port. Pruning the tree inside the extension cut the payload from 806 KB to 111 KB but only moved the time to 246 ms. The rest is chrome.debugger materialising the nodes in the service worker. If your agent reads the accessibility tree on every step, this is the number to design around, not the ping.

One command is 300 times slower when the window is not in front. Input.dispatchMouseEvent through the bridge took 5,016 ms when Chrome's window was behind another application, because the renderer waits for an input acknowledgement it never gets. Sending Emulation.setFocusEmulationEnabled first brought it to 16.4 ms, reproducibly (A-B-A). If you only test with Chrome in the foreground, you will not see this until a user does.

The host must translate request IDs. Each client numbers its own requests from 1. Our first host forwarded the client's ID to the extension unchanged, which worked for the first client by coincidence and silently dropped every reply to the second. It looked like "the service worker stopped responding" and cost half an hour of suspecting Chrome.

Native messaging caps a host-to-extension message at 1 MB. Our own framing allowed 128 MiB, so an upload path that worked for an 8 KB avatar disconnected the bridge at 926 KB. For files, use DOM.setFileInputFiles, which sends a path and no bytes.

What chrome.debugger cannot do

The extension API exposes 26 CDP domains and no Browser.*. In practice that means you translate a few calls: Target.createTarget becomes chrome.tabs.create, Target.activateTarget becomes chrome.tabs.update({active: true}), window bounds go through chrome.windows, and SystemInfo.getProcessInfo is simply unavailable. Everything on the hot path (DOM, Runtime, Input, Accessibility, Page) passes straight through. Note also that --load-extension was removed in Chrome 137, so until an extension is on the Web Store, the one manual step is loading it unpacked from chrome://extensions. And while a task runs, Chrome shows the "is debugging this browser" bar; that is the honest signal that something is driving the tab.

How Computron uses this

Computron's default connection is exactly this bridge: your Chrome, your profile, your logins, with the debug port kept only as a last-resort fallback that it names in one line when it has to use it, because the fallback is a different, logged-out browser. The extension attaches to a tab only while a task you started is running and detaches after. Everything it records stays on your machine as plain text. If you want to see the host, the extension and the CDP translation table, the source is on GitHub.