Todos os artigos

Como a gente impede que um plugin de IA roube nossos dados

Skills e plugins rodam com as suas permissões. Como configurar sua IA para deixar credenciais e segredos fora do alcance de scripts maliciosos.

Keit Ollé
Keit Ollé

Cofundadora & CTO na Solu

10 de julho de 2026 · 8 min de leitura

Esta semana vi um comentário que resume um risco que, na minha opinião, deveria preocupar qualquer pessoa que utiliza agentes de IA localmente:

Pasted image 20260710145055.png

"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 ou um MCP Server de terceiros executa 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.

Vou usar o Claude Code nos exemplos porque é a ferramenta que utilizo no dia a dia. Os nomes dos arquivos e campos pertencem a ele, mas o princípio vale para qualquer agente que execute comandos localmente, como Codex, Cursor ou Gemini CLI.

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.

A lógica está invertida: em vez de proteger o dado, você está tentando prever todas as formas possíveis de exfiltração.

Proteja o alvo, não a saída

Em vez de tentar listar todos os jeitos 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.

Vale uma observação importante. A regra Read() protege tanto a ferramenta de leitura do Claude quanto comandos que ele reconhece, como cat, head, tail e sed. Ela não impede, porém, que um subprocesso abra o arquivo diretamente, como um script Python usando open(".env"). Esse cenário só é bloqueado corretamente pelo sandbox, 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. Não usta 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:

  1. Managed (política da organização, instalada como arquivo de sistema, via MDM por exemplo), não pode ser sobrescrita

  2. Argumentos de linha de comando

  3. .claude/settings.local.json do projeto (fora do git)

  4. .claude/settings.json do projeto (no git)

  5. ~/.claude/settings.json do 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 de permissions.deny que impeçam a leitura de .env, ~/.ssh, ~/.aws e qualquer pasta secrets. 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.