Skip to main content
Blog

Remote debugging with authorization in the browser

Updated at October 7, 2026

Brave
CDP
Agents
Security

I replaced the open CDP port on each Brave profile with Chromium remote debugging that asks for authorization, and fixed two bugs so chrome-devtools-axi could connect to it.

This post follows One browser per profile for AI agents on Hyprland. There I split each Brave profile into its own process, with one CDP port per profile, so AI agents would hit the right browser. That post ended with a warning. The CDP port has no authentication, and any process running as my user could control all three logged-in profiles.

Now the three profiles start without a debugging port. When an agent wants to use the browser, Brave shows an authorization prompt and only allows the connection if I accept.

How big the problem was

With Brave started with --remote-debugging-port=9222, a curl with no credentials already got an answer:

curl -s http://127.0.0.1:9222/json/version

The response includes the browser WebSocket address. With that address, a script can open tabs, read cookies and run JavaScript on any site where I am logged in. That applies to an AI agent, to a compromised npm package, or to any other program running as my user.

The port only listens on localhost, so nobody outside the machine can reach it. The risk is in the programs already running here.

Chromium remote debugging with authorization

Chromium has a second way to turn on remote debugging, through the chrome://inspect/#remote-debugging page. In Brave the address is brave://inspect/#remote-debugging. The same page exists in Chrome, Edge and Vivaldi, each with its own internal prefix, such as edge://inspect. The option exists from Chrome 144 on, and the Brave I use is based on Chromium 154.

I turn the option on once per profile. Brave saves the choice in the Local State file of the data folder, so it stays on after restarting the browser:

"devtools": { "remote_debugging": { "user-enabled": true } }

With the option on, the behavior changes in three ways:

  • The HTTP discovery endpoints stop answering. The same curl as before gets a 404.
  • Brave writes the connection address to the DevToolsActivePort file inside the profile data folder.
  • Every new WebSocket connection opens an authorization prompt in the browser.

DevToolsActivePort has two lines, the port and the WebSocket path:

9222
/devtools/browser/0981ab15-ef84-48ba-b46d-30d06ca90cf7
sequenceDiagram
  accTitle: An agent connecting to Brave with authorization
  accDescr: The agent reads DevToolsActivePort in the profile folder, opens the WebSocket and Brave asks for authorization. After I accept, the axi bridge keeps the connection open for the next calls.
  participant A as Agent
  participant X as chrome-devtools-axi
  participant B as Brave personal
  participant E as Me
  A->>X: axi-brave personal newpage URL
  X->>X: reads ~/.config/brave-personal/DevToolsActivePort
  X->>B: opens ws://127.0.0.1:9222/devtools/browser/ID
  B->>E: asks for authorization
  E->>B: allows
  B-->>X: connection accepted
  A->>X: next calls
  X->>B: reuses the same connection

Launchers without a port

The first change was removing the flag from the three scripts in ~/.local/bin:

 exec /usr/bin/brave \
   --user-data-dir="$HOME/.config/brave-bliss" \
   --profile-directory="Profile 1" \
-  --remote-debugging-port=9223 \
   "$@"

Then I closed the personal Brave, opened it from the launcher and turned the option on at brave://inspect/#remote-debugging. ss showed Brave listening on 9222 without the flag on the command line. The curl on /json/version started getting a 404.

chrome-devtools-axi stopped working

chrome-devtools-axi is the CLI my agents use to control the browser. It wraps chrome-devtools-mcp, Google’s MCP server for Chrome DevTools. The axi starts a background process, the bridge, and the bridge starts chrome-devtools-mcp.

I hit two problems before the connection worked. The first one already existed before this change and only showed up when I tried to connect to the running Brave.

The bridge hid the error

The axi only returned this:

error: "Bridge for session \"brave-personal\" exited with code 1 before becoming ready on port 9357"
code: BRIDGE_NOT_READY

The bridge is started with stdio: "ignore", so the error output from chrome-devtools-mcp is lost. I ran the bridge script in the foreground, with the same environment variables, and the real error showed up:

Arguments executablePath and browserUrl are mutually exclusive

My Omarchy setup has no Chrome installed. That is why CHROME_DEVTOOLS_AXI_MCP_PATH points to a script of mine, mcp-runner.mjs, which adds --executablePath with the Brave path. That works when the axi opens its own browser. When it connects to a running browser, it passes --browserUrl, and chrome-devtools-mcp rejects both options together.

The runner now skips --executablePath when the connection goes to an existing browser:

const connectsToExisting = process.argv.slice(2).some(a =>
  /^--(browserUrl|wsEndpoint|autoConnect)\b/.test(a));

const args = [
  mcpScript,
  ...(connectsToExisting ? [] : [`--executablePath=${browserPath}`]),
  ...process.argv.slice(2)
];

autoConnect looked for Chrome

For debugging with authorization, chrome-devtools-mcp has the --autoConnect option. It reads DevToolsActivePort from the data folder given in --userDataDir. Without that folder, it looks for the default Chrome.

The axi has CHROME_DEVTOOLS_AXI_AUTO_CONNECT=1, but it does not pass the profile folder along. I tried adding --userDataDir in the runner, reading the CHROME_DEVTOOLS_AXI_USER_DATA_DIR variable. The connection kept failing, and the bridge health check showed why:

Could not find DevToolsActivePort for chrome at /home/satin/.config/google-chrome/DevToolsActivePort

The variable never reached the runner. The bridge starts chrome-devtools-mcp with the MCP SDK StdioClientTransport. When nobody provides an environment, the SDK builds its own with only a few variables. On Linux those are HOME, LOGNAME, PATH, SHELL, TERM and USER.

The way out was passing the address as an argument, which the bridge forwards without filtering. The axi accepts a ws:// URL in CHROME_DEVTOOLS_AXI_BROWSER_URL and turns it into --wsEndpoint. The full address is already in DevToolsActivePort.

The axi-brave shortcut

So agents would not build the URL by hand, I created ~/.local/bin/axi-brave:

#!/usr/bin/env bash
set -euo pipefail
profile="${1:-}"
case "$profile" in
  personal|bliss|unicesumar) shift ;;
  *) echo "usage: axi-brave <personal|bliss|unicesumar> <axi command>" >&2; exit 2 ;;
esac
portfile="$HOME/.config/brave-$profile/DevToolsActivePort"
if [[ ! -s "$portfile" ]] || ! pgrep -f -- "--user-data-dir=$HOME/.config/brave-$profile" >/dev/null; then
  echo "Brave $profile is closed or has remote debugging off." >&2
  exit 3
fi
port=$(sed -n 1p "$portfile"); path=$(sed -n 2p "$portfile")
export CHROME_DEVTOOLS_AXI_SESSION="brave-$profile"
export CHROME_DEVTOOLS_AXI_BROWSER_URL="ws://127.0.0.1:${port}${path}"
export CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS="${CHROME_DEVTOOLS_AXI_BRIDGE_TIMEOUT_MS:-120000}"
exec chrome-devtools-axi "$@"

The default bridge timeout is 30 seconds, too short for me to see the prompt and click. The shortcut raises it to 2 minutes.

$ axi-brave personal newpage https://palpitae.com.br/
page:
  title: Palpitae | Bolão de futebol online grátis com os amigos
  url: "https://palpitae.com.br/"

The first call took 10 seconds, the time it took me to accept the prompt. The bridge keeps the WebSocket open, so the second call answered right away and asked nothing. A new prompt only shows up when the bridge goes idle and closes the connection.

One rule for every agent

I use Claude Code, Codex, Cursor CLI, OpenCode, Gemini CLI and others. They all read the same instruction file, ~/.agents/CONTEXT.md, through a symlink or an import. The browser rule lives there, and the profile comes from the project folder:

Project folderProfile
~/Work/bliss and work foldersaxi-brave bliss
~/Work/personalaxi-brave personal
~/Work/unicesumaraxi-brave unicesumar
any other folderthe agent asks

The same rule tells the agent to warn me before the first call and forbids opening a port or restarting the browser on its own.

The browser extension and the axi

The Claude extension for Chrome does not use CDP. It talks to Claude Code through native messaging and already asked for permission per site. Even so, I use the axi for most tasks, for two reasons.

The first is reach. The Claude extension only serves Claude Code, and the ChatGPT one only serves OpenAI products. The axi is a terminal command, and any agent with a shell can call it.

The second is token use. The extension relies a lot on screenshots, and every image goes into the model context. The axi returns compact text, and its screenshot command saves the image to a file. The image only goes into the context if the agent opens the file.

Browser extensionchrome-devtools-axi
Who can use itonly the vendor’s agentany agent with a shell
Connectionnative messaging and accountCDP through DevToolsActivePort
Screenshotcomes back as an image in the contextsaved to a file
DevTools featuresconsoleconsole, network, performance, Lighthouse

What stays the same

Authorization protects against a random program trying to use the port without me noticing. It does not create a barrier between processes of the same user. A malicious program running as my user can still read the profile folder on disk. No browser setting fixes that.

The setup also depends on details an update can change. mcp-runner.mjs points to an internal path of chrome-devtools-mcp, and axi-brave reads the current DevToolsActivePort format. If either changes, the axi stops connecting. In both cases the browser does not go back to accepting connections without a prompt.

References