Um navegador por perfil para agentes de IA no Hyprland
Atualizado em 6 de outubro de 2026
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

Isso resolve o foco. O perfil errado continua errado, porque ainda é o mesmo processo do Brave.
Uma pasta de dados por perfil com Btrfs reflink
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.

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.
Seletor de links com Quickshell
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.

Links do terminal e do resto do sistema
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.

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.