Vi um comentário essa semana que tem fundamento e deveria ser a preocupação de todos:
"Depois que eu li sobre injeção de malware nesses repos de skill eu tenho um maior medo de baixar skill dos outros e lançarem um curl esquisito, tava inclusive criando um setup pra testar de forma segura [...]"
Uma skill, um plugin, um MCP server de terceiro roda com as mesmas permissões que você. Se ele dispara um comando, o comando roda na sua máquina, com suas credenciais, seu ~/.ssh, seu .env. Ninguém precisa te hackear, você baixou e mandou rodar.
Aqui vou usar o Claude Code nos exemplos, é o que uso no dia a dia, então os nomes de arquivo e de campo são dele. Mas o raciocínio vale pra qualquer harness de IA que roda comando na sua máquina, Codex, Cursor e os outros. A sintaxe muda, o princípio é o mesmo.
A boa notícia é que dá pra blindar isso pelo settings.json. A má notícia é que a receita que circula por aí está errada, ou pelo menos incompleta. Todo mundo fala em bloquear o curl e para por aí. Vou te mostrar por que isso não te salva sozinho, e o que realmente protege.
Por que bloquear o curl é peneira
O instinto é óbvio. Adiciona uma regra de deny pro curl e pronto:
{
"permissions": {
"deny": ["Bash(curl:*)"]
}
}
Isso funciona pra curl https://infostealer.com. O problema é que a regra faz prefix-matching, ela casa com o começo do comando. E existe um número absurdo de jeitos de mandar dados pra fora que não começam com a palavra curl. wget. /usr/bin/curl com caminho absoluto. nc. python -c "import urllib.request; ...". node -e "fetch(...)". Cada um desses fura sua regra.
Você nunca vai enumerar todas as formas de vazar dado. É uma corrida que você perde. Uma blocklist de comando é uma peneira, e o atacante só precisa achar um furo. A lógica está invertida.
Proteja o alvo, não a saída
Em vez de tentar listar todo jeito de vazar, proteja o que você não quer que seja lido. Isso é uma lista curta e fechada, ao contrário da lista de comandos que é infinita.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Read(**/secrets/**)"
]
}
}
Agora não importa se a skill tenta curl, wget ou qualquer outro comando pra mandar o .env pra fora. Ela não consegue nem ler o arquivo. Você fechou a origem do dado, não a porta de saída.
Os padrões de caminho seguem a mesma sintaxe do .gitignore. ~/ é o home, ./ é a raiz do projeto onde o settings.json mora, // é a raiz do sistema, ** casa qualquer profundidade. No meu settings de usuário as regras são na forma Read(//**/.env), que pega o arquivo em qualquer canto da máquina, não só no projeto aberto. E vale saber que o deny segue symlink. Se um link aponta pra um arquivo negado, o link também é negado. Não dá pra driblar com atalho.
Um detalhe honesto aqui, porque faz diferença. A regra Read() cobre a ferramenta de leitura de arquivo do Claude e também os comandos de leitura que o Claude Code reconhece no Bash, tipo cat, head, tail e sed. O que ela não cobre é subprocesso que abre o arquivo por conta própria. Um script Python ou Node que dá open(".env") não passa por essa checagem, e essa via só o sandbox fecha de verdade, no nível do sistema operacional.
Mesmo assim eu não dependo do que a ferramenta reconhece ou deixa de reconhecer. No meu settings de usuário tem mais de cinquenta regras de deny explícitas, cada comando de leitura cruzado com cada alvo. Um recorte real da minha lista:
"deny": [
"Bash(cat *.env:*)",
"Bash(head *.env:*)",
"Bash(grep *.pem:*)",
"Bash(less *credentials:*)",
"Bash(cat *id_rsa*:*)",
"Bash(env:*)",
"Bash(printenv:*)"
]
cat, head, tail, grep, less, more e diff, cada um contra .env, .pem, .key, credencial e chave ssh. Custa nada manter e é garantia explícita, escrita por mim, não inferida pela ferramenta. E os dois últimos fecham outra porta, env e printenv despejam as variáveis de ambiente, e tem segredo que mora ali, não em arquivo.
Mas repara, tudo isso ainda é lista. O que não está em nenhuma lista, um leitor obscuro, um script que abre o arquivo direto, passa. É por isso que as camadas de baixo existem, o hook que olha o comando inteiro e o sandbox que fecha no sistema operacional.
A ordem importa, e o deny sempre ganha
As três listas de permissions são avaliadas numa ordem fixa. Primeiro deny, depois ask, depois allow. A primeira que casa decide. Ou seja, um deny amplo vence um allow específico, você não consegue liberar por acidente algo que negou antes.
No meu settings isso aparece na prática. Bash(cat:*) está no allow, o agente usa cat o dia inteiro sem me pedir nada. E Bash(cat *.env:*) está no deny. Resultado, cat funciona em tudo, menos em cima de segredo. Liberei a ferramenta, não o alvo.
O ask é o meio-termo. Ele força uma aprovação toda vez, sem exceção. Se você quer permitir rede mas com você no loop, é o campo certo:
{
"permissions": {
"ask": ["Bash(curl:*)", "Bash(wget:*)"]
}
}
Isso não bloqueia, mas te obriga a olhar cada chamada antes de acontecer. Contra uma skill que você não confia totalmente, "me pergunta sempre" já muda o jogo. O comando esquisito aparece na sua frente antes de rodar, não depois.
Ninguém quer aprovar comando o dia inteiro
Tem uma tensão real aqui, e prefiro assumir do que fingir que não existe. Esse post inteiro é sobre travar coisa, e a gente vive exatamente a era de soltar o agente, de deixar ele trabalhar meia hora sozinho e voltar com a tarefa pronta. Cada "posso rodar isso?" quebra esse fluxo. E aprovação demais vira ritual, depois da vigésima pergunta do dia você aprova sem ler. Proteção que você aprova sem ler não protege nada.
A saída não é afrouxar, é mudar a decisão de lugar. O deny bem feito é o que te compra liberdade no resto. Você decide uma vez, no arquivo, o que nunca pode acontecer, e solta o agente com um allow generoso no dia a dia sabendo que o piso está travado. É assim que eu trabalho, o agente roda git, npm e todo comando de leitura sem me perguntar, porque o que não pode já está negado. O ask fica pro que é raro e de alto impacto, tipo rede pra fora. O resto é liberdade.
O nível mais forte é um hook que inspeciona antes de rodar
Regra de permissão é estática, ela olha o começo do comando. Um hook PreToolUse é código seu que roda antes de cada ferramenta e pode olhar o comando inteiro, com toda a lógica que você quiser, e vetar.
É aqui que você pega os casos que o prefix-matching não pega. Um script que recebe o comando pelo stdin, procura por qualquer padrão suspeito no texto todo, e mata com exit 2:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# pega exfiltração independente de como foi escrita
if echo "$COMMAND" | grep -qiE '(curl|wget|nc|/dev/tcp)'; then
echo "Comando de rede bloqueado pelo hook. Revise manualmente." >&2
exit 2
fi
exit 0
Registra no settings.json mirando os comandos Bash:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh" }
]
}
]
}
}
O exit 2 é o que bloqueia, e o que você escreve no stderr volta como feedback. A diferença pro deny é que aqui você olha o comando inteiro com grep, não só o prefixo, então /usr/bin/curl e python -c "urllib..." também caem se você escrever o padrão certo. E o hook compõe com as regras, não substitui. Um deny continua bloqueando mesmo que o hook deixe passar, e um bloqueio do hook vence um allow. As camadas se somam.
Um detalhe que vale ouro. O hook roda o SEU código, então você controla 100% da lógica. Dá pra logar toda tentativa num arquivo, pedir confirmação só pra domínio desconhecido, liberar github.com e barrar o resto. É o ponto onde a política deixa de ser uma lista e vira programa.
Quase ninguém configura a precedência dos arquivos
Isso tudo perde o sentido se qualquer um pode sobrescrever. Os arquivos de settings têm hierarquia, e o de cima ganha:
- Managed (política da organização, instalada como arquivo de sistema, via MDM por exemplo), não pode ser sobrescrita
- Argumentos de linha de comando
-
.claude/settings.local.jsondo projeto (fora do git) -
.claude/settings.jsondo projeto (no git) -
~/.claude/settings.jsondo usuário
A regra é a mesma do deny. Se algo foi negado em qualquer nível, nenhum outro nível consegue liberar.
Na prática isso significa duas coisas. Se você é a única pessoa da máquina, põe suas regras de proteção no ~/.claude/settings.json, elas valem pra todo projeto. Se você administra um time, o nível managed é onde você trava o que ninguém pode afrouxar, nem sem querer, nem de propósito.
Ação prática
Combinando as camadas, essa é uma base defensável pro cenário do comentário, uma skill de terceiro que você não auditou linha por linha. Cola no ~/.claude/settings.json:
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Read(**/secrets/**)"
],
"ask": [
"Bash(curl:*)",
"Bash(wget:*)"
]
}
}
Isso já te dá o essencial. O dado sensível não é legível pela via mais comum, e qualquer rede pelo caminho óbvio te pede aprovação antes de rodar. Pra quem quer a camada forte, o hook PreToolUse é o próximo passo, e é ele que fecha os furos que a regra de prefixo deixa aberto.
Se quiser transformar isso em ação direto na sua máquina, cola este prompt no Claude Code:
Leia meu
~/.claude/settings.json. Some (sem apagar o que já existe) regras depermissions.denyque impeçam a leitura de.env,~/.ssh,~/.awse qualquer pastasecrets. Me mostre o diff antes de salvar.
O que o settings.json NÃO resolve
Fecho com a parte honesta, porque parar aqui seria vender proteção completa. O settings.json reduz muito o risco, mas não é uma jaula. O comando roda na sua máquina, e uma blocklist é sempre uma corrida contra quem escreve o próximo jeito de driblar.
E dá pra ir um passo além. Se o segredo nem existe em arquivo, não tem o que roubar. Um gerenciador como o 1Password CLI guarda no .env só uma referência e injeta o segredo de verdade como variável de ambiente na hora de rodar, com op run. O agente pode até ler o arquivo, o que tem lá é ponteiro, não chave.
Pra isolamento de verdade, no nível do sistema operacional, com controle real de rede e de credencial, existe o sandbox. Por enquanto, proteja o alvo em vez da saída, deixa o deny ganhar, e põe um hook no caminho crítico. É o que trava o piso uma vez e te deixa soltar o agente no resto. Já é muito mais do que a maioria das configs por aí.
Como eu disse, os exemplos são do Claude Code, mas o princípio não é dele. Codex, Cursor, Gemini CLI, todo harness de IA que executa comando na sua máquina tem o mesmo tipo de trava, modo de aprovação por comando, negação de comando, algum sandbox. Os nomes de arquivo e de campo mudam, o raciocínio de "proteja o alvo, não a saída" é o mesmo em qualquer um. Aprende num, adapta pros outros.
