Pular para o conteúdo principal
Blog

Depuração remota do navegador com autorização

Atualizado em 7 de outubro de 2026

Brave
CDP
Agentes
Segurança

Troquei a porta CDP aberta de cada perfil do Brave pela depuração remota com autorização do Chromium e corrigi dois bugs para o chrome-devtools-axi conectar nela.

Este post continua o Um navegador por perfil para agentes de IA no Hyprland. Lá eu separei cada perfil do Brave em um processo próprio, com uma porta CDP por perfil, para os agentes de IA acertarem o navegador. O post terminava com um aviso. A porta CDP não tem autenticação, e qualquer processo do meu usuário controlava os três perfis logados.

Hoje os três perfis abrem sem porta de depuração. Quando um agente quer usar o navegador, o Brave mostra um pedido de autorização e só libera a conexão se eu aceitar.

O tamanho do problema

Com o Brave aberto com --remote-debugging-port=9222, um curl sem nenhuma credencial já respondia:

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

A resposta traz o endereço WebSocket do navegador. Com esse endereço, um script consegue abrir abas, ler cookies e executar JavaScript em qualquer site onde estou logado. Isso vale para um agente de IA, para um pacote npm comprometido ou para qualquer outro programa que rode com o meu usuário.

A porta escuta só em localhost, então ninguém de fora da máquina chega nela. O risco está nos programas que já rodam aqui.

A depuração remota com autorização do Chromium

O Chromium tem um segundo jeito de ligar a depuração remota, pela página chrome://inspect/#remote-debugging. No Brave o endereço é brave://inspect/#remote-debugging. O mesmo caminho existe no Chrome, no Edge e no Vivaldi, cada um com o seu prefixo interno, como edge://inspect. A opção existe a partir do Chrome 144, e o Brave que uso é baseado no Chromium 154.

Ativo a opção uma vez em cada perfil. O Brave guarda a escolha no Local State da pasta de dados, então ela continua ligada depois de reiniciar o navegador:

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

Com a opção ligada, o comportamento muda em três pontos:

  • Os endpoints HTTP de descoberta param de responder. O mesmo curl de antes recebe 404.
  • O Brave escreve o endereço da conexão no arquivo DevToolsActivePort, dentro da pasta de dados do perfil.
  • Cada conexão nova ao WebSocket abre um pedido de autorização no navegador.

O DevToolsActivePort tem duas linhas, a porta e o caminho do WebSocket:

9222
/devtools/browser/0981ab15-ef84-48ba-b46d-30d06ca90cf7
sequenceDiagram
  accTitle: Conexão de um agente ao Brave com autorização
  accDescr: O agente lê o DevToolsActivePort na pasta do perfil, abre o WebSocket e o Brave pede autorização. Depois que eu aceito, a ponte da axi mantém a conexão aberta para as próximas chamadas.
  participant A as Agente
  participant X as chrome-devtools-axi
  participant B as Brave personal
  participant E as Eu
  A->>X: axi-brave personal newpage URL
  X->>X: lê ~/.config/brave-personal/DevToolsActivePort
  X->>B: abre ws://127.0.0.1:9222/devtools/browser/ID
  B->>E: pede autorização
  E->>B: permite
  B-->>X: conexão aceita
  A->>X: próximas chamadas
  X->>B: usa a mesma conexão

Lançadores sem porta

A primeira mudança foi tirar a flag dos três scripts em ~/.local/bin:

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

Depois fechei o Brave pessoal, abri pelo lançador e ativei a opção em brave://inspect/#remote-debugging. O ss mostrou o Brave escutando na 9222 sem a flag na linha de comando. O curl em /json/version passou a receber 404.

O chrome-devtools-axi parou de funcionar

O chrome-devtools-axi é a CLI que meus agentes usam para controlar o navegador. Ele é um wrapper do chrome-devtools-mcp, o servidor MCP do Google para o Chrome DevTools. A axi sobe um processo em segundo plano, a ponte, e a ponte inicia o chrome-devtools-mcp.

Tive dois problemas até a conexão funcionar. O primeiro já existia antes da mudança e só apareceu quando tentei conectar no Brave aberto.

A ponte escondia o erro

A axi devolvia só isto:

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

A ponte é iniciada com stdio: "ignore", então a saída de erro do chrome-devtools-mcp some. Rodei o script da ponte em primeiro plano, com as mesmas variáveis de ambiente, e o erro real apareceu:

Arguments executablePath and browserUrl are mutually exclusive

O meu Omarchy não tem o Chrome instalado. Por isso a variável CHROME_DEVTOOLS_AXI_MCP_PATH aponta para um script meu, o mcp-runner.mjs, que acrescenta --executablePath com o caminho do Brave. Isso funciona quando a axi abre um navegador próprio. Quando ela conecta num navegador já aberto, passa --browserUrl, e o chrome-devtools-mcp recusa as duas opções juntas.

O runner passou a pular o --executablePath quando a conexão é com um navegador existente:

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

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

O autoConnect procurava o Chrome

Para a depuração com autorização, o chrome-devtools-mcp tem a opção --autoConnect. Ela lê o DevToolsActivePort da pasta de dados indicada em --userDataDir. Sem essa pasta, ela procura o Chrome padrão.

A axi tem CHROME_DEVTOOLS_AXI_AUTO_CONNECT=1, mas não repassa a pasta do perfil. Tentei acrescentar o --userDataDir no runner, lendo a variável CHROME_DEVTOOLS_AXI_USER_DATA_DIR. A conexão continuou falhando, e o health check da ponte mostrou o motivo:

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

A variável não chegava ao runner. A ponte inicia o chrome-devtools-mcp com o StdioClientTransport do SDK do MCP. Quando ninguém informa o ambiente, o SDK monta um ambiente próprio com poucas variáveis. No Linux são só HOME, LOGNAME, PATH, SHELL, TERM e USER.

A saída foi passar o endereço como argumento, que a ponte repassa sem filtro. A axi aceita uma URL ws:// em CHROME_DEVTOOLS_AXI_BROWSER_URL e a transforma em --wsEndpoint. O endereço completo já está no DevToolsActivePort.

O atalho axi-brave

Para os agentes não montarem a URL na mão, criei o ~/.local/bin/axi-brave:

#!/usr/bin/env bash
set -euo pipefail
profile="${1:-}"
case "$profile" in
  personal|bliss|unicesumar) shift ;;
  *) echo "uso: axi-brave <personal|bliss|unicesumar> <comando da axi>" >&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 fechado ou sem depuração remota." >&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 "$@"

O prazo padrão da ponte é de 30 segundos, pouco para eu ver o pedido e clicar. O atalho sobe esse prazo para 2 minutos.

$ 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/"

A primeira chamada levou 10 segundos, o tempo de eu aceitar o pedido. A ponte mantém o WebSocket aberto, então a segunda chamada respondeu na hora e não pediu nada. Um pedido novo só aparece quando a ponte fica ociosa e fecha a conexão.

Uma regra para todos os agentes

Uso o Claude Code, o Codex, o Cursor CLI, o OpenCode, o Gemini CLI e outros. Todos leem o mesmo arquivo de instruções, o ~/.agents/CONTEXT.md, por symlink ou import. A regra do navegador ficou lá, e o perfil sai da pasta do projeto:

Pasta do projetoPerfil
~/Work/bliss e pastas do trabalhoaxi-brave bliss
~/Work/personalaxi-brave personal
~/Work/unicesumaraxi-brave unicesumar
outra pastao agente pergunta

A mesma regra manda o agente me avisar antes da primeira chamada e proíbe abrir porta ou relançar o navegador por conta própria.

A extensão do navegador e a axi

A extensão do Claude para Chrome não usa CDP. Ela fala com o Claude Code por native messaging e já pedia permissão por site. Mesmo assim, uso a axi em boa parte das tarefas, por dois motivos.

O primeiro é o alcance. A extensão do Claude só atende o Claude Code, e a do ChatGPT só atende os produtos da OpenAI. A axi é um comando de terminal, e qualquer agente com shell consegue chamar.

O segundo é o consumo de tokens. A extensão trabalha muito com screenshot, e cada imagem entra no contexto do modelo. A axi devolve texto compacto, e o comando screenshot salva a imagem em arquivo. Ela só entra no contexto se o agente abrir o arquivo.

Extensão do navegadorchrome-devtools-axi
Quem usasó o agente do fabricantequalquer agente com shell
Conexãonative messaging e contaCDP pelo DevToolsActivePort
Screenshotvolta como imagem no contextosalva em arquivo
Ferramentas de DevToolsconsoleconsole, rede, performance, Lighthouse

O que continua igual

A autorização protege contra um programa qualquer que tente usar a porta sem eu ver. Ela não cria uma barreira entre processos do mesmo usuário. Um programa malicioso rodando com o meu usuário ainda consegue ler a pasta do perfil no disco. Para isso não há configuração de navegador que resolva.

A solução também depende de detalhes que uma atualização pode mudar. O mcp-runner.mjs aponta para um caminho interno do chrome-devtools-mcp, e o axi-brave lê o formato atual do DevToolsActivePort. Se algum deles mudar, a axi para de conectar. Nenhum dos dois casos volta a abrir o navegador sem pedido.

Referências