Pular para o conteúdo principal
Blog

Um navegador por perfil para agentes de IA no Hyprland

Atualizado em 6 de outubro de 2026

Brave
Hyprland
Omarchy
Agentes

Como separei os perfis do Brave em processos próprios no Omarchy, com Btrfs reflink, uma porta CDP por perfil e um seletor de links em Quickshell.

Uso o Brave com três perfis: o pessoal, o do trabalho e o da faculdade onde dou aula. Deixo cada perfil do Brave em um workspace separado do Hyprland. Quando comecei a pedir para agentes de IA mexerem em páginas onde estou logado, o fluxo começou a quebrar.

Eram dois problemas:

  • Um link aberto no terminal caía na janela do Brave que teve foco por último, muitas vezes no perfil errado.
  • O agente abria a página no perfil sem o meu login e falhava no meio da tarefa.

A solução que uso hoje separa cada perfil em um processo próprio do Brave. Cada processo tem a sua pasta de dados, a sua porta de depuração e o seu host de native messaging. Um seletor em Quickshell decide em qual perfil cada link abre.

O Brave é o navegador que eu uso, mas tudo aqui vale para qualquer navegador baseado em Chromium, como Chrome, Edge e Vivaldi. As flags e os arquivos internos são os mesmos.

flowchart LR
  accTitle: Caminho de um link e de um agente até o perfil certo do Brave
  accDescr: Links do terminal foot e de outros aplicativos passam pelo seletor em Quickshell, que abre o perfil escolhido. Agentes acessam cada perfil pela sua porta CDP ou pelo host de native messaging configurado na pasta de dados do perfil.
  FOOT["foot<br/>Ctrl+Shift+O"] --> PICKER["Seletor Quickshell<br/>teclas 1, 2 e 3"]
  XDG["xdg-open<br/>outros apps"] --> PICKER
  PICKER --> BP["brave-personal<br/>CDP 9222"]
  PICKER --> BW["brave-bliss<br/>CDP 9223"]
  PICKER --> BU["brave-unicesumar<br/>CDP 9225"]
  AXI["chrome-devtools-axi"] -->|CDP| BW
  CW["Claude Code<br/>~/.claude-w"] -->|native messaging| BW
  CP["Claude Code<br/>~/.claude"] -->|native messaging| BP

O que eu não sabia sobre o Chromium

O Brave é baseado no Chromium, e o Chromium roda um único processo principal por pasta de dados (--user-data-dir). Os perfis que aparecem no avatar do canto da janela são subpastas dessa mesma pasta, como Default, Profile 1 e Profile 2. Para o sistema, os três perfis são um único programa.

Dentro da pasta de dados ficam o SingletonLock e o SingletonSocket. Quando rodo brave https://exemplo.com com o Brave já aberto, o novo processo encontra a trava, manda a URL pelo socket para o processo que já existe e encerra. O processo principal abre a aba na janela que ele considera ativa.

Passar --profile-directory="Profile 1" não muda isso, pois o comando continua indo para o mesmo processo. Foi aqui que entendi a raiz do problema. Enquanto os três perfis morassem na mesma pasta de dados, qualquer ferramenta externa falaria com o mesmo processo.

O foco entre workspaces no Hyprland

O Hyprland é um compositor Wayland de janelas lado a lado (tiling), escrito em C++. Ele não minimiza janelas. Cada janela fica em um workspace numerado, e eu troco de workspace pelo teclado.

O Omarchy, a configuração de Arch Linux com Hyprland que uso, liga a opção misc:focus_on_activate. Com ela ligada, quando um aplicativo pede ativação, o Hyprland pula para a janela dele. Para automação de navegador isso é ruim, porque o agente rouba o teclado enquanto estou digitando em outro lugar.

Desliguei a opção no ~/.config/hypr/looknfeel.lua:

hl.config({
  misc = {
    focus_on_activate = false,
  },
})

Com a opção desligada, o Hyprland só marca a janela como urgente. Para não perder o aviso, um script lê o socket de eventos do Hyprland e mostra uma notificação dizendo qual workspace pediu foco:

socat -U - "UNIX-CONNECT:$sock" | while IFS= read -r line; do
  case $line in
    urgent\>\>*) notify_for "${line#urgent>>}" & ;;
  esac
done

Notificação "Brave pede foco no workspace 10" no canto da tela, com o foco ainda no terminal do workspace 9.

Isso resolve o foco. O perfil errado continua errado, porque ainda é o mesmo processo do Brave.

Para cada perfil virar um processo próprio, cada um precisa de um --user-data-dir diferente:

  • ~/.config/brave-personal
  • ~/.config/brave-bliss
  • ~/.config/brave-unicesumar

Copiar os perfis do jeito comum ocuparia vários GB de cache, histórico, IndexedDB e extensões. A minha pasta original do Brave tinha 6,1 GiB.

O Omarchy instala o sistema em Btrfs. O Btrfs é um sistema de arquivos copy-on-write (cópia na escrita) e suporta reflink. Um reflink cria um arquivo novo que aponta para os mesmos blocos do original. Os dados só são duplicados quando um dos lados altera um bloco.

O script de migração rodou com o Brave fechado. Para cada perfil ele copia com cp -a --reflink=always os arquivos compartilhados da pasta original, como o Local State, e a subpasta daquele perfil. Depois ajusta o Local State da cópia para listar só aquele perfil. A cópia do trabalho, por exemplo, recebe a subpasta Profile 1:

cp -a --reflink=always \
  ~/.config/BraveSoftware/Brave-Browser/"Profile 1" \
  ~/.config/brave-bliss/"Profile 1"

Um dia depois da migração, o btrfs filesystem du mostra o resultado:

     Total   Exclusive  Set shared  Filename
   4.25GiB   458.43MiB     3.28GiB  /home/satin/.config/brave-personal
   1.41GiB   125.82MiB     1.00GiB  /home/satin/.config/brave-bliss
   1.48GiB    86.09MiB     1.08GiB  /home/satin/.config/brave-unicesumar

As três pastas somam 7,1 GiB, mas só 670 MiB são blocos exclusivos delas. O resto continua compartilhado com a pasta original, que mantive como backup.

Uma porta CDP por perfil

O Chrome DevTools Protocol (CDP) é o protocolo de depuração do Chromium. Ele expõe uma API por WebSocket para ler o DOM, executar JavaScript, acompanhar a rede e tirar screenshots. Ferramentas como o Playwright e o chrome-devtools-axi controlam o navegador por ele.

Para abrir essa porta, o navegador precisa subir com --remote-debugging-port. Desde o Chrome 136, essa flag é ignorada na pasta de dados padrão. Ela só funciona com um --user-data-dir diferente do padrão, e o Brave segue a mesma regra do Chromium. A separação das pastas também era necessária para isso.

Cada perfil ganhou um script em ~/.local/bin. O do trabalho ficou assim:

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

O pessoal usa Default e a porta 9222. O da faculdade usa Profile 2 e começou na porta 9224.

A 9224 deu conflito. O bridge do chrome-devtools-axi escuta na 9224 por padrão. Como ele subiu antes, o Brave só conseguiu abrir a porta em IPv6 ([::1]:9224), e quem chamasse 127.0.0.1:9224 falava com o bridge em vez do navegador. Mudei o perfil da faculdade para a 9225.

Terminal com os processos do Brave, cada um com o seu --user-data-dir e --remote-debugging-port, a saída do ss mostrando o conflito na porta 9224 com o bridge do chrome-devtools-axi, e os manifestos de native messaging apontando para ~/.claude-w e ~/.claude.

Com as portas separadas, escolho o perfil pela URL:

CHROME_DEVTOOLS_AXI_BROWSER_URL="http://127.0.0.1:9223" chrome-devtools-axi pages

A porta CDP não tem autenticação. Qualquer processo do meu usuário consegue controlar os três perfis logados, inclusive o do trabalho. A porta escuta só em localhost, mas vale saber disso antes de copiar a ideia.

A extensão do Claude e o native messaging

A extensão do Claude no navegador não usa CDP. Ela conversa com o Claude Code por native messaging. O navegador inicia um programa local descrito em um manifesto JSON e troca mensagens com ele pela entrada e saída padrão.

No Linux, o Chromium procura esses manifestos em NativeMessagingHosts/, dentro da pasta de dados. Com uma pasta por perfil, cada instância pode apontar para um Claude Code diferente. Uso um diretório de configuração separado para o trabalho (~/.claude-w), e o manifesto do brave-bliss aponta para ele:

{
  "name": "com.anthropic.claude_code_browser_extension",
  "path": "/home/satin/.claude-w/chrome/chrome-native-host",
  "type": "stdio"
}

Os perfis pessoal e da faculdade apontam para o ~/.claude.

Ainda há um ponto em aberto. Os sockets do bridge ficam todos em /tmp/claude-mcp-browser-bridge-satin/, nomeados pelo PID do host, e não pelo diretório de configuração. Não testei com duas sessões do Claude Code abertas ao mesmo tempo, uma em cada perfil, para confirmar que cada uma escolhe o socket certo.

Com três navegadores separados, faltava decidir para onde vai um link clicado fora deles.

A interface do Omarchy, como a barra e os menus, é feita em Quickshell. O Quickshell é um toolkit para montar componentes de desktop em QML (Qt Modeling Language) no Wayland. A janela vira uma camada do compositor pelo protocolo wlr-layer-shell, por cima das outras janelas.

A primeira tentativa usou o Walker, o lançador de aplicativos do Omarchy. Ficou com um visual diferente do resto do sistema. A segunda usou o omarchy-menu-select, que já tem a cara do Omarchy, mas exigia apertar Enter depois do número. O que eu queria era apertar 1, 2 ou 3 e o link abrir na hora.

Acabei escrevendo um seletor próprio em ~/.config/omarchy/browser-picker/shell.qml. Este é o trecho que decide o que abrir:

ShellRoot {
  id: shell
  property string url: Quickshell.env("PICKER_URL") || ""

  PanelWindow {
    anchors { top: true; bottom: true; left: true; right: true }
    color: "transparent"
    WlrLayershell.layer: WlrLayer.Overlay
    WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive

    Item {
      id: winRoot
      focus: true

      function select(browserName) {
        var args = ["/home/satin/.local/bin/" + browserName];
        if (shell.url.length > 0) args.push(shell.url);
        Quickshell.execDetached(args);
        close();
      }

      function close() {
        Quickshell.execDetached(["sh", "-c", "pkill -f 'quickshell -p .*browser-picker'"]);
      }

      Keys.onPressed: function(event) {
        if (event.key === Qt.Key_1) winRoot.select("brave-personal");
        else if (event.key === Qt.Key_2) winRoot.select("brave-bliss");
        else if (event.key === Qt.Key_3) winRoot.select("brave-unicesumar");
        else if (event.key === Qt.Key_Escape) winRoot.close();
      }
    }
  }
}

O arquivo completo ainda tem o card com as três opções, navegação pelas setas e clique com o mouse. Como o menu está na camada Overlay com foco exclusivo do teclado, a tecla não vaza para a janela de trás.

Seletor "Choose browser profile" aberto por cima do terminal, com as opções 1 Personal, 2 Bliss e 3 Unicesumar.

O foot é o terminal que uso no Omarchy. O atalho Ctrl+Shift+O é do próprio foot e ativa o modo de URLs. Ele marca cada link visível com uma letra, e basta digitar a letra para abrir o link.

Terminal foot no modo de URLs, com a letra S marcando o link da pull request.

Por padrão o foot abre o link com xdg-open. Troquei pelo meu script no ~/.config/foot/foot.ini:

[url]
launch=omarchy-launch-browser ${url}

O omarchy-launch-browser é o comando que o Omarchy usa para abrir o navegador, inclusive no Super+Shift+B. Criei uma versão em ~/.local/bin, que vem antes no PATH. Ela passa a URL para o QML por variável de ambiente:

export PICKER_URL="$url"
PICKER_FILE="$HOME/.config/omarchy/browser-picker/shell.qml"
pkill -f "quickshell -p .*browser-picker" 2>/dev/null || true
exec quickshell -p "$PICKER_FILE"

Para os links de outros aplicativos, criei o omarchy-launch-browser.desktop e registrei ele como navegador padrão:

xdg-mime default omarchy-launch-browser.desktop x-scheme-handler/http
xdg-mime default omarchy-launch-browser.desktop x-scheme-handler/https
xdg-mime default omarchy-launch-browser.desktop text/html

Dentro do próprio Brave, uma extensão pequena adiciona o item “Open in profile” no menu do botão direito. Ela manda a URL por native messaging para um script que chama o brave-personal, o brave-bliss ou o brave-unicesumar. Quando estou lendo algo no perfil pessoal que pertence ao trabalho, abro no outro perfil sem copiar a URL.

As imagens deste post saíram do fluxo completo. Apertei Ctrl+Shift+O no terminal do workspace 9, digitei a letra do link e escolhi 1. A aba abriu no Brave pessoal, que fica no workspace 10, e o foco ficou no terminal com a notificação no canto.

Referências