name: browser-actions description: Operacional do Princípio 12 (browser-as-agent). Setup do MCP Playwright com Chrome dedicado, auth assistida via cookie pre-check, blocklist de Zona 3 (irreversíveis), lock multi-slot, troubleshooting. Use quando a escada de precedência cair em (4) Playwright MCP.
browser-actions
Operacional pra Claude agir em nome do usuário via browser, quando CLI/MCP/API de domínio falham. Governado pelo Princípio 12 da CLAUDE.md.
A precedência é definida no Princípio 12 — esta skill cobre só o "como" do nível (4) Playwright MCP. Não revisita o "quando".
Pareia com browser-inspect (leitura runtime + execução efêmera de debug, sem mutação real no mundo). Esta skill (browser-actions) cobre ações mutativas (cliques de pagamento, submits, navegação destrutiva). Pra leitura de console.log, network requests, evaluate read-only ou synthetic clicks de debug → ~/.claude/skills/browser-inspect/SKILL.md.
Quando usar / quando NÃO usar
Use quando:
- Não existe CLI de domínio capaz da operação (ou ela está em modo (b)/(c)).
- Não existe MCP de domínio capaz (idem).
- Não existe API documentada/permitida (idem).
- O alvo é uma UI web e a ação não tem endpoint público.
NÃO use quando:
- A operação é puramente de leitura local (Read, Bash, Grep resolvem).
- Existe
gh,aws,netlify,stripe,gcloud,kubectl, etc. com o subcomando alvo. Sempre tente CLI primeiro. - O usuário pediu "abre no browser" como navegação humana — abrir manual, sem MCP.
- O alvo está na blocklist e não há confirmação humana — pausar e perguntar.
Setup operacional
MCP registrado no escopo user apontando pro wrapper de lock:
claude mcp remove playwright
claude mcp add --transport stdio --scope user playwright -- \
/Users/alex/.claude/skills/browser-actions/wrapper-launch.sh
Wrapper internamente lança:
npx -y @playwright/mcp@latest \
--user-data-dir "$HOME/.claude/chrome-profile-<slot>" \
--browser chrome
Onde <slot> é resolvido per-call: $CLAUDE_SLOT_NAME se setada, ou basename($PWD) quando bate o padrão <repo>-<N> (pool Boris), ou "default" (fallback para repos diretos / worktrees / sessões sem pool). Profile é criado on-demand — primeira sessão em cada slot exige re-login nos sites usados (cookies não compartilham entre slots).
Pré-requisitos:
/Applications/Google Chrome.app/instalado.- Wrapper executável (
chmod +x).
Validação: claude mcp list | grep playwright deve mostrar ✓ Connected.
Protocolo de auth assistida
Antes de qualquer ação que assume sessão autenticada, validar que a sessão está ativa — não confiar em cookies herdados do perfil.
Sinais de auth — ordenados por confiabilidade
1. Redirect pra login após browser_navigate no endpoint privado alvo (sinal mais forte).
Tentar a navegação direto. Se a URL final contém /login, /signin, /auth/, ou a resposta é 401/403, a sessão está ausente/expirada. Não dá falso positivo. Validado 2026-05-06: github.com/settings/repositories → github.com/login?return_to=... quando deslogado; URL idêntica preservada quando logado.
2. Meta tag de identidade (forte quando o site expõe).
Sites bem comportados expõem <meta name="user-login" content="..."> (GitHub) ou similar (og:profile, etc.). Presente = logado; ausente em endpoint privado = deslogado. Validado 2026-05-06 (user_login: "alexlopespereira" apareceu na meta após login).
3. document.cookie pre-check (sinal fraco — só triagem).
Validado empiricamente em 2026-05-06: dá falso positivo (cookies de tracking populam mesmo deslogado — _octo, cpu_bucket, tz no GitHub) e falso negativo (cookies de sessão modernos são httpOnly/SameSite=Strict e não aparecem em document.cookie, ex: user_session, _gh_sess). Não confie isoladamente.
Sequência recomendada
1. browser_navigate(url=endpoint privado alvo)
2. browser_evaluate(() => ({
url: location.href,
userMeta: document.querySelector('meta[name="user-login"]')?.content
}))
3. Se URL final contém /login OU userMeta ausente em endpoint privado:
→ ANUNCIAR: "Vou abrir [domínio] no Chrome dedicado. Faça login e me avise quando estiver pronto."
→ Esperar resposta do usuário no chat ("ok", "pronto", ENTER).
→ Re-navegar. Se ainda redireciona, perguntar de novo.
4. Se sessão ativa: prosseguir com a ação alvo.
Anti-pattern
browser_evaluate(() => document.cookie) como único sinal — falha silenciosa em ambas as direções.
Auth ausente NUNCA é skip pro próximo nível (regra do Princípio 12 — "auth ausente nunca é skip"). Sempre pede ao usuário.
Cadência do anúncio
Anuncia abertura do browser:
- Primeira chamada de browser na conversa — sempre.
- Mudança de eTLD+1 — re-anuncia. Exemplos:
accounts.google.com→mail.google.com— sem reanúncio (mesmogoogle.com).github.com→accounts.google.com— com reanúncio (mudou o registrable domain).
Extração do eTLD+1
Antes de decidir reanunciar, executar o algoritmo passo-a-passo no raciocínio (Chain-of-Thought explícito). A regra é determinística — basta seguir as 3 etapas na ordem.
Exercitada empiricamente — vide tabela de casos de teste abaixo.
Algoritmo (3 etapas, ordem importa)
- IP literal ou
localhost→ host único:eTLD+1 = hostname(sem fatiamento). Detecção: regex^[0-9.:]+$para IP, ou hostname ==localhost. Hostnames com:porta(ex:localhost:3000) são tratados peloURL.hostnameque já remove a porta — usarURL.hostnamecomo entrada. - Suffix multi-label conhecido (tabela abaixo) → últimos 3 labels. Match:
hostnametermina com.<suffix>(ponto antes —gov.brdireto NÃO bate em*.gov.br, ver nota). Se hostname tem <3 labels, cai no fallback (etapa 3). - Fallback → últimos 2 labels (
hostname.split('.').slice(-2).join('.')). Cobre.com/.org/.net/.io/etc.
Tabela de suffixes multi-label conhecidos (top-N curado)
| Suffix | Origem | Uso esperado |
|---|---|---|
*.com.br |
BR comercial | bancos, e-commerce, serviços online BR |
*.gov.br |
BR governo | gov serviços |
*.org.br |
BR org | ONGs, comunidades |
*.net.br |
BR rede | infraestrutura |
*.edu.br |
BR educação | universidades |
*.co.uk |
UK comercial | empresas UK |
*.ac.uk |
UK acadêmico | universidades UK |
*.gov.uk |
UK governo | gov UK |
*.com.au |
AU comercial | empresas AU |
*.com.mx |
MX comercial | empresas MX |
*.co.jp |
JP comercial | empresas JP |
Tabela de casos de teste (oráculo)
| URL | Hostname | Algoritmo aplicado | eTLD+1 esperado |
|---|---|---|---|
https://github.com/user/repo |
github.com |
etapa 3 (slice -2) | github.com |
https://accounts.google.com/signin |
accounts.google.com |
etapa 3 | google.com |
https://mail.google.com/inbox |
mail.google.com |
etapa 3 | google.com |
https://www.bbc.co.uk/news |
www.bbc.co.uk |
etapa 2 (*.co.uk) |
bbc.co.uk |
https://www.itau.com.br/empresas |
www.itau.com.br |
etapa 2 (*.com.br) |
itau.com.br |
https://gov.br/saude |
gov.br |
etapa 2 não bate → fallback | gov.br |
https://192.168.1.1/foo |
192.168.1.1 |
etapa 1 (IP) | 192.168.1.1 |
https://localhost:3000/dev |
localhost |
etapa 1 (host único) | localhost |
https://api.itau.com.br/v1 |
api.itau.com.br |
etapa 2 | itau.com.br |
https://www.uol.com.br/noticias |
www.uol.com.br |
etapa 2 | uol.com.br |
https://www.bb.com.br/pessoas |
www.bb.com.br |
etapa 2 | bb.com.br |
https://www.example.com.au/ |
www.example.com.au |
etapa 2 (*.com.au) |
example.com.au |
https://www.example.com.mx/ |
www.example.com.mx |
etapa 2 (*.com.mx) |
example.com.mx |
https://www.example.co.jp/ |
www.example.co.jp |
etapa 2 (*.co.jp) |
example.co.jp |
https://algo.empresa.com.tr/ |
algo.empresa.com.tr |
fail-safe (≥3 labels, suffix não listado, última label 2 letras) | reanúncio conservador |
https://api.atlassian.com/v1 |
api.atlassian.com |
etapa 3 (fail-safe NÃO dispara: com tem 3 letras) |
atlassian.com |
Nota sobre gov.br direto vs *.gov.br
gov.br (host único, portal federal) é uma entidade distinta de saude.gov.br (subdomínio governamental). O algoritmo trata corretamente:
gov.br→ etapa 2 não bate (precisa de.gov.brcom algo antes), cai no fallback (etapa 3) →gov.br.saude.gov.br→ etapa 2 bate em*.gov.br→saude.gov.br.
Resultado: navegar de gov.br para saude.gov.br produz eTLD+1 diferentes — reanúncio dispara, que é o comportamento correto (são entidades distintas).
Fail-safe — detector mecânico
Se hostname não bate em nenhuma entrada da tabela de suffixes E tem ≥3 labels E sua última label tem exatamente 2 letras (provável ccTLD), reanunciar conservadoramente em vez de aplicar slice(-2) cegamente.
Mecanismo: ≥3 labels + suffix não listado + última label com 2 letras é o sinal de "possível ccTLD multi-label que não está na tabela top-N". TLDs ccTLD são quase todos códigos ISO 3166-1 alpha-2 (2 letras: .tr, .cn, .in, .fr...); gTLDs históricos têm 3+ letras (.com, .org, .net, .io, .app...). A condição "última label = 2 letras" filtra subdomínios de gTLD comuns: accounts.google.com → última label com (3 letras) → fail-safe NÃO dispara → cai corretamente na etapa 3 → google.com. Custo de falso positivo (reanunciar quando não precisava) = uma linha extra de mensagem. Custo de falso negativo (silêncio em mudança real de eTLD+1) = violação silenciosa do Princípio 12. Otimizar para fail-safe.
Exemplos:
algo.qualquer.com.tr(Turquia, fora da tabela) → 4 labels, suffix*.com.trnão listado, última labeltr(2 letras) → fail-safe dispara, reanunciar.accounts.google.com→ 3 labels, suffix*.comnão está na tabela, mas última labelcom(3 letras) → fail-safe NÃO dispara → etapa 3 →google.com(consistente com os casos de teste para*.google.com).
Suposições abertas (registradas como risco conhecido)
- [SUPOSIÇÃO] Os 11 suffixes cobrem ≥95% do uso esperado. Aceito como risco conhecido — se a regra falhar em sessões reais com >5% de frequência, expandir a tabela (ex:
*.com.tr,*.com.cn,*.co.insob demanda). - [SUPOSIÇÃO] Fail-safe (reanunciar em dúvida) não vira spam de reanúncio. Aceito como risco conhecido sem ação preventiva — se usuário reclamar de reanúncios redundantes, ajustar.
Anúncio template: "Vou abrir {eTLD+1} no Chrome dedicado em ~/.claude/chrome-profile/."
Blocklist de Zona 3 (irreversíveis / sensíveis)
Pausa e pergunta humano antes de clicar/submeter quando qualquer dos critérios bate. Hook PreToolUse enforça subset crítico mecanicamente; modelo é responsável pelos demais.
| Critério | Detecção | Ação |
|---|---|---|
accessibleName do botão contém trigger word (PT+EN) |
Snapshot + match case-insensitive em blocklist.json:trigger_words |
Pergunta humano antes do click |
| Form contém campo de pagamento | Input com name=cardnumber|cvv|cvc|cc-number (ver payment_form_inputs) |
Pergunta humano antes do submit |
| URL bate com domínio sensível | Regex em blocklist.json:sensitive_domains_regex (*.stripe.com, *.bank*, *.itau*, *.bb.com.br, *.santander*) |
Pergunta humano em toda ação mutativa |
| Modal de confirmação nativa | window.confirm, role=alertdialog |
Nunca auto-aceita; sempre pergunta |
| Healthcare / saúde | URL ou conteúdo com termos médicos sensíveis (subjetivo — modelo decide) | Pergunta humano |
Trigger words PT+EN (lista canônica em blocklist.json):
delete, excluir, remover, remove, cancel subscription, cancelar assinatura,
unsubscribe, pay, pagar, buy, comprar, purchase, subscribe, assinar,
publish, publicar, post, postar, send, enviar, make public, tornar público,
transfer, transferir, withdraw, sacar, confirm purchase
Regra: quando bater, pausar e dizer textualmente "Vou clicar em '{accessibleName}' em {url} — confirma?". Esperar ok/sim antes de prosseguir. Recusa = aborta a ação.
Exceções (a)–(e) do Princípio 11 também se aplicam. Blocklist do P12 é adicional, não substitui.
Profile per-slot (workflow Boris)
Workflow Boris Cherny tem 5 checkouts paralelos (~/Projects/checkouts/<repo>-{1..5}). 2+ sessões Claude Code podem tentar usar o browser ao mesmo tempo. Solução: profile Chrome separado por slot — cada slot usa ~/.claude/chrome-profile-<slot>/ independente. Sem fila, sem competição entre slots.
wrapper-launch.sh resolve <slot> em ordem:
$CLAUDE_SLOT_NAME(env var, override explícito — use quando rodar fora de pool).basename($PWD)quando bate<repo>-<N>regex (pool Boris)."default"(fallback para repos diretos, worktrees, sessões sem pool).
Lock per-profile (~/.claude/chrome-profile-<slot>/.lock) serializa acesso ao mesmo profile. Quando 2+ sessões Claude Code resolvem o mesmo slot (comum — ex.: systemd auto-start + sessão manual no mesmo checkout, ou subagentes), a 2ª não falha mais: cai para um profile efêmero exclusivo da sessão (~/.claude/chrome-profile-<slot>-pid<PID>/, removido no exit), e emite AVISO: slot '...' já em uso ... usando profile efêmero no stderr. Custo: essa 2ª sessão não herda cookies; ganho: playwright funciona em paralelo, sem MCP error -32000: Connection closed. Trap remove o lock (e o profile efêmero) em SIGINT/TERM/HUP; SIGKILL deixa órfão (detectado na próxima invocação via kill -0 no PID gravado, ou contornado pelo fallback). Regressão coberta por bin/tests/test_wrapper_launch.sh AC6.
Trade-off aceito: cookies/login não compartilham entre slots. Primeira sessão em cada slot exige re-auth nos sites usados. Ganho: zero risco de corromper Chrome user-data por escrita concorrente; sintoma "Playwright inacessível em quase todas as sessões" elimina.
Lock travado num slot específico: rmdir ~/.claude/chrome-profile-<slot>/.lock. Recovery global (todos os slots + processos zumbi): bash ~/.claude/skills/browser-actions/reset.sh.
Limpeza de MCP duplicado em checkouts (one-shot): bash ~/.claude/skills/browser-actions/cleanup-stale-mcp.sh — remove entries playwright legadas dos .mcp.json per-checkout (mantém só o wrapper user-scope global).
Troubleshooting
| Problema | Sintoma | Resolução |
|---|---|---|
| Captcha (reCAPTCHA, hCaptcha) | Página exibe widget de captcha | Pausar, dizer "tem captcha, resolve aí pra mim e me avise". Esperar humano. |
| 2FA por SMS / authenticator | Após login, página pede código | Idem captcha — pausa + pede. |
| Sessão expirada | Cookie presente, mas resposta 401/403 ou redirect pra /login | Trigger fluxo de re-auth (anuncia + espera login). |
document.cookie vazio (ou só com tracking) mesmo logado |
Cookies de sessão são httpOnly/SameSite=Strict |
Validado 2026-05-06. Sinais primários: redirect-pra-login ou meta user-login (ver "Protocolo de auth assistida"). document.cookie só serve como triagem. |
| localStorage-only auth (raro) | Sem cookies, auth via JWT em localStorage | Aceitar fricção: pre-check via browser_evaluate(() => localStorage.getItem('token')). |
| Extensões bloqueando seletores | Snapshot retorna estranho, click falha em elementos visíveis | Lock garante que é Chrome dedicado sem extensões — descartar essa hipótese cedo. Se acontecer mesmo assim, abrir DevTools e inspecionar. |
| MCP não conecta após reconfig | claude mcp list mostra erro de timeout/conexão |
(1) wrapper executável? ls -l wrapper-launch.sh. (2) lock órfão em algum slot? bash ~/.claude/skills/browser-actions/reset.sh. (3) Chrome instalado? ls /Applications/Google\ Chrome.app. (4) MCP duplicado em .mcp.json do checkout? claude mcp list mostra 2 entries playwright → rodar bash ~/.claude/skills/browser-actions/cleanup-stale-mcp.sh. |
--browser chrome falha |
MCP sobe mas browser não abre | Fallback: editar wrapper pra usar --executable-path "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" (S4 do handoff). |
Delimitação vs skill testing
A skill testing tem a regra "Playwright CLI > MCP" — isso se refere ao test runner (npx playwright test), pra executar suítes de teste e2e.
Browser-as-agent é categoria distinta: controle de browser via Playwright MCP pra agir em nome do usuário em sites externos. Governado por este SKILL.md + Princípio 12.
Não há conflito — são domínios diferentes.
Acumulação do perfil dedicado
~/.claude/chrome-profile/ acumula cookies, localStorage, IndexedDB, histórico, indefinidamente. Não rotaciona automaticamente.
Limpeza geral (força re-login em todos os slots): rm -rf ~/.claude/chrome-profile-*/. Cada slot recria o profile na próxima invocação. Sugerido trimestralmente ou após exposição (laptop emprestado, suspeita de comprometimento). Limpeza por slot: rm -rf ~/.claude/chrome-profile-<slot>/.
Limpeza por domínio (futuro): granularidade por host ainda não existe. Manualmente: abrir o perfil, DevTools → Application → Cookies → deletar.
Manutenção do perfil
Dois slash commands operam direto no SQLite de cookies (~/.claude/chrome-profile/Default/Cookies):
/browser-status— read-only. Abre o DB com URIfile:...?mode=ro&immutable=1(não disputa lock, não cria journal/-wal/-shm) e imprime tabela agregada porhost_key: DOMAIN, COUNT, LAST_ACCESS (datetime localtime), EXPIRES (date ousession). Detecta MCP rodando via~/.claude/chrome-profile/.lock/pid+kill -0e avisa que cookies dos últimos segundos podem não aparecer no snapshot. Se o perfil ainda não foi inicializado (Default/Cookies inexistente), imprime mensagem amigável e exit 0./browser-logout-all [--confirm]— destrutivo. Sem--confirm: preview (status do lock + contagem). Com--confirme lock liberado:PRAGMA wal_checkpoint(TRUNCATE)→DELETE FROM cookies→VACUUMem batch. Aborta com exit 1 se MCP vivo (concorrência em WAL pode corromper). VACUUM é necessário porque DELETE só marca páginas como livres — sem VACUUM o arquivo não shrinka.
Escopo restrito — apenas cookies HTTP. Local Storage, IndexedDB e cache do browser não são afetados. Para reset total, encerre o MCP e rode:
rm -rf ~/.claude/chrome-profile/
mkdir -p ~/.claude/chrome-profile
chmod 700 ~/.claude/chrome-profile
Site list explícito
Sites em que Claude já operou via este perfil (manter manualmente nesta seção; futuro: auto-incrementar):
- (vazio — ainda não operou em nenhum site após Fatia 1)
Quando Claude operar num novo domínio (eTLD+1), adicionar uma linha aqui com data + propósito principal.
Referência rápida
- Princípio 12 —
CLAUDE.md(escada CLI > MCP > API > Playwright MCP). - Hook de blocklist —
~/.claude/hooks/block-browser-mutations.sh. Allowlist por sessão para e2e em ambiente de teste:BBM_ALLOW_TRIGGERS="comprar,pay"(case-insensitive, trim) remove só esses termos; demais triggers seguem ativos. - Lockfile per-slot —
~/.claude/chrome-profile-<slot>/.lock. - Perfis per-slot —
~/.claude/chrome-profile-<slot>/(<slot>resolvido em runtime). - Wrapper —
~/.claude/skills/browser-actions/wrapper-launch.sh. - Recovery global —
~/.claude/skills/browser-actions/reset.sh. - Patch de MCP duplicado em checkouts —
~/.claude/skills/browser-actions/cleanup-stale-mcp.sh. - Blocklist estrutural —
~/.claude/skills/browser-actions/blocklist.json.