Voltar
|
#seguranca #claude-code #devtools #configuracao

2,243 palavras · 12 min de leitura

Claude Code: as configurações de segurança que protegem seu código de si mesmo

A maior parte do que vou explicar aqui veio do curso oficial da Anthropic sobre Claude Code. Recomendo pra quem quer entender o ferramental completo. O que fiz foi aplicar no meu workflow e adicionar camadas extras de proteção que fazem sentido pro meu contexto.

No artigo anterior, falei sobre como usar skills pra transformar o Claude Code num auditor de segurança. Mas tem um problema anterior a isso: o próprio Claude Code tem acesso ao seu sistema de arquivos. Ele lê arquivos, executa comandos, escreve código. Se não estiver configurado direito, ele pode ler seu .env, exibir suas chaves de API no output, ou rodar um rm -rf sem pensar duas vezes.

A segurança do código que o Claude Code audita não adianta nada se o ambiente onde ele roda não for seguro.

As três raízes de configuração

O Claude Code lê configurações de três lugares, em ordem de prioridade:

1. ~/.claude/settings.json (global) — Vale pra todos os projetos. É onde ficam regras pessoais: permissões padrão, hooks globais, preferências de workflow. Fica na home e não é versionado.

2. .claude/settings.json (projeto) — Fica na raiz do repositório. Sobrescreve o global. É onde ficam regras específicas do projeto: deny rules pra arquivos sensíveis daquele repo, hooks de validação, restrições de time. Versione no git. Assim todos que trabalham no repositório herdam as mesmas restrições.

3. CLAUDE.md (instruções em linguagem natural) — Também na raiz do repositório. Não é um JSON de permissões. São instruções em markdown que o Claude Code absorve no início de cada sessão. Regras de estilo, convenções de código, e sim, regras de segurança.

A hierarquia funciona assim: o global define o baseline. O projeto restringe ou ajusta. O CLAUDE.md complementa com contexto semântico que o JSON não consegue expressar.

Permissions: allow e deny

A estrutura principal é o campo permissions, com duas listas: allow e deny.

{
  "permissions": {
    "allow": [
      "Bash(*)",
      "Read(*)",
      "Edit(*)",
      "Write(*)",
      "Glob(*)",
      "Grep(*)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)",
      "Read(./config/credentials.json)"
    ]
  }
}

O allow define o que o Claude Code pode fazer sem pedir confirmação. O deny bloqueia absolutamente. Deny sempre ganha de allow. Não importa se o allow tem Read(*). Se o deny tem Read(./.env), o .env não é lido.

Protegendo variáveis de ambiente

O cenário mais comum: o Claude Code precisa ler seu código pra entender o projeto, mas o .env tem chaves de banco de dados, tokens de API, secrets de autenticação. Se ele ler o .env e depois for escrever um teste ou documentar algo, aquelas chaves podem aparecer no output, no código gerado, ou pior, num commit.

Mas bloquear só a leitura não basta. O Claude Code também pode sobrescrever um .env existente ou criar um novo com valores errados. As deny rules precisam cobrir as três operações: Read, Write e Edit.

"deny": [
  "Read(./.env)",
  "Read(./.env.*)",
  "Write(./.env)",
  "Write(./.env.*)",
  "Edit(./.env)",
  "Edit(./.env.*)"
]

O padrão .env.* cobre .env.local, .env.production, .env.staging, .env.development. Mas se preferir ser explícito, liste cada um. Explícito é mais seguro que glob quando se trata de secrets.

Protegendo diretórios de secrets

Muitos projetos mantêm certificados, chaves privadas ou arquivos de credenciais em diretórios dedicados. A mesma lógica: bloquear Read, Write e Edit.

"deny": [
  "Read(./secrets/**)",
  "Write(./secrets/**)",
  "Edit(./secrets/**)",
  "Read(./config/credentials.json)",
  "Write(./config/credentials.json)",
  "Edit(./config/credentials.json)",
  "Read(./**/*.pem)",
  "Read(./**/*.key)",
  "Read(./**/*serviceAccountKey*)",
  "Read(./**/credentials*.json)"
]

O ** faz match recursivo. Read(./**/*.pem) bloqueia qualquer arquivo .pem em qualquer subdiretório. Assim, mesmo que alguém adicione um certificado numa pasta inesperada, o Claude Code não lê.

Bloqueando comandos destrutivos

O allow para Bash aceita glob patterns no argumento. Em vez de Bash(*), que permite qualquer comando, é possível ser mais restritivo:

"allow": [
  "Bash(npm test*)",
  "Bash(npm run*)",
  "Bash(git status*)",
  "Bash(git diff*)",
  "Bash(git log*)",
  "Bash(ls*)",
  "Bash(cat package.json)"
]

Sem o Bash(*), qualquer comando que não esteja na lista de allow vai pedir confirmação antes de executar. Isso significa que rm, curl, docker, kubectl e qualquer outro comando potencialmente destrutivo precisa de aprovação explícita.

Mesmo com Bash(*) liberado, o deny pode bloquear comandos específicos:

"deny": [
  "Bash(rm -rf *)",
  "Bash(git push --force*)",
  "Bash(git reset --hard*)"
]

Essa é a abordagem que faz mais sentido pra quem já tem experiência. Liberar tudo e bloquear o que é perigoso.

Hooks: a segunda camada de proteção

Deny rules protegem por path e por comando. Mas e se o Claude Code ler um secret de outra fonte (um log, um output de comando, uma resposta de API) e tentar escrevê-lo num arquivo de código? O deny por path não pega isso. O hook pega.

Hooks são scripts que rodam automaticamente antes ou depois de ações do Claude Code. O formato no settings.json usa matcher pra filtrar a ferramenta e hooks como array de handlers:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/meu-script.sh"
          }
        ]
      }
    ]
  }
}

O script recebe o input da ferramenta via stdin como JSON. Pra bloquear uma ação, o script retorna um JSON com permissionDecision: "deny". Pra permitir, basta retornar exit code 0.

Script: detectando secrets no conteúdo

#!/bin/bash
# ~/.claude/hooks/block-secrets.sh
# Ignora arquivos markdown (podem conter exemplos de patterns)
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.filePath // ""' 2>/dev/null)

if echo "$FILE_PATH" | grep -qiE '\.(md|mdx|txt|rst)$'; then
  exit 0
fi

CONTENT=$(jq -r '.tool_input | to_entries | map(.value) | join(" ")' 2>/dev/null)

if echo "$CONTENT" | grep -qiE 'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|mongodb\+srv://|BEGIN.*PRIVATE KEY|xox[bpas]-'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Secret detectado no conteudo"
    }
  }'
else
  exit 0
fi

Esse hook roda antes de qualquer Write, Edit ou MultiEdit. Verifica se o conteúdo contém patterns de AWS Access Keys, OpenAI/Stripe keys, GitHub tokens, connection strings MongoDB, chaves privadas e Slack tokens. Se detectar, bloqueia a escrita.

Detalhe importante: o script ignora arquivos .md e .mdx. Sem isso, um blog post que mencione esses patterns como exemplo (como este que estou escrevendo agora) seria bloqueado. Na prática, isso aconteceu comigo ao escrever este artigo. O hook bloqueou a escrita do próprio post porque o conteúdo continha os patterns de exemplo. A solução foi adicionar o filtro por extensão no topo do script.

Script: protegendo .env via Write/Edit

#!/bin/bash
# ~/.claude/hooks/block-env-write.sh
FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.filePath // ""' 2>/dev/null)

if echo "$FILE_PATH" | grep -qE '\.env($|\.)'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Escrita em arquivo .env bloqueada por hook"
    }
  }'
else
  exit 0
fi

Script: protegendo .env via Bash

O deny bloqueia Read(./.env) e Write(./.env), mas o Claude Code pode tentar cat .env ou echo "KEY=value" >> .env pelo Bash. O hook pega esses caminhos indiretos:

#!/bin/bash
# ~/.claude/hooks/block-env-bash.sh
COMMAND=$(jq -r '.tool_input.command' 2>/dev/null)

if echo "$COMMAND" | grep -qiE '>\s*\.env|>>\s*\.env|cat\s+\.env|head\s+\.env|tail\s+\.env|source\s+\.env|cp\s.*\.env|mv\s.*\.env|printenv'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Comando manipula ou expoe arquivo .env"
    }
  }'
else
  exit 0
fi

Script: audit log

Um hook PostToolUse que loga todos os comandos Bash executados. Não bloqueia nada, só registra:

#!/bin/bash
# ~/.claude/hooks/audit-log.sh
COMMAND=$(jq -r '.tool_input.command // "unknown"' 2>/dev/null)
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | Bash | $(echo "$COMMAND" | head -c 200)" >> ~/.claude/audit.log
exit 0

A configuração dos hooks no settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-secrets.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-env-write.sh"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-env-bash.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/audit-log.sh"
          }
        ]
      }
    ]
  }
}

Cada matcher filtra a ferramenta. O array hooks contém os handlers com type: "command" e o path do script. Os scripts leem JSON do stdin com jq e retornam JSON com permissionDecision: "deny" pra bloquear ou exit code 0 pra permitir.

A configuração completa que uso

{
  "permissions": {
    "allow": [
      "Bash(*)",
      "Read(*)",
      "Edit(*)",
      "Write(*)",
      "MultiEdit(*)",
      "LS(**)",
      "Glob(*)",
      "Grep(*)",
      "NotebookRead(*)",
      "NotebookEdit(*)",
      "TodoRead(*)",
      "TodoWrite(*)",
      "WebSearch(*)",
      "WebFetch(*)"
    ],
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Write(./.env)",
      "Write(./.env.*)",
      "Edit(./.env)",
      "Edit(./.env.*)",
      "Read(./secrets/**)",
      "Write(./secrets/**)",
      "Edit(./secrets/**)",
      "Read(./config/credentials.json)",
      "Write(./config/credentials.json)",
      "Edit(./config/credentials.json)",
      "Read(./**/*.pem)",
      "Read(./**/*.key)",
      "Read(./**/*serviceAccountKey*)",
      "Read(./**/credentials*.json)",
      "Bash(rm -rf *)",
      "Bash(git push --force*)",
      "Bash(git reset --hard*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-secrets.sh"
          }
        ]
      },
      {
        "matcher": "Write|Edit|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-env-write.sh"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/block-env-bash.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/audit-log.sh"
          }
        ]
      }
    ]
  }
}

Sim, o allow é amplo. Bash(*), Read(*), Write(*). Tudo liberado. Trabalho sozinho nos meus projetos, conheço o que cada comando faz, e travar o Claude Code pra pedir confirmação a cada ls destrói a produtividade.

Mas permissão ampla não significa sem proteção. O deny list bloqueia os arquivos sensíveis (.env, secrets, credenciais, chaves privadas). Os hooks validam o conteúdo antes de qualquer escrita e bloqueiam comandos que manipulam .env pelo shell. O audit log registra tudo que foi executado.

A diferença entre ser permissivo por descuido e ser permissivo de forma controlada é ter as camadas de proteção configuradas. O Bash(*) sem deny rules e sem hooks é um risco. O Bash(*) com deny list, hooks de validação e audit log é uma decisão consciente de quem entende os vetores de ataque e escolheu onde colocar as barreiras.

CLAUDE.md: a terceira camada

O CLAUDE.md na raiz do projeto complementa o settings.json com regras em linguagem natural. O JSON define permissões binárias (pode/não pode). O markdown define comportamento (como agir).

## Segurança

- Nunca incluir secrets, tokens ou chaves em código ou documentação
- Nunca fazer commit de arquivos .env
- Usar variáveis de ambiente (process.env) para toda configuração sensível
- Nunca logar dados sensíveis (senhas, tokens, PII)
- Sanitizar todo input antes de usar em queries

O Claude Code lê essas regras no início de cada sessão e as aplica em tudo que faz. Se for criar um teste que precisa de uma API key, vai usar process.env.API_KEY ou um mock em vez de um valor hardcoded.

O CLAUDE.md pega o que o JSON não consegue expressar. "Nunca logar dados sensíveis" não é uma permissão. É um comportamento. O Claude Code entende isso e aplica em todo código que escreve.

O que acontece se não configurar nada

Sem deny rules e sem hooks, o Claude Code pode:

  1. Ler o .env quando tentar entender a configuração do projeto
  2. Incluir valores reais de variáveis de ambiente em exemplos de código
  3. Exibir connection strings no output ao explicar como o banco funciona
  4. Copiar secrets pra arquivos de teste ou documentação
  5. Sobrescrever o .env com valores diferentes ao tentar "ajudar"
  6. Rodar cat .env pelo Bash, contornando deny rules de Read

Nenhum desses cenários é malicioso. O Claude Code não está tentando roubar secrets. Ele simplesmente não sabe que aquele arquivo não deveria ser lido a menos que exista uma regra dizendo isso.

A configuração padrão do Claude Code é conservadora. Ele pede confirmação pra quase tudo. Mas quando as permissões são liberadas pra aumentar a produtividade (o que é normal e recomendado), o deny list e os hooks se tornam a última linha de defesa.

As quatro camadas

Resumindo, a proteção funciona em camadas:

  1. Deny rules por path — bloqueia Read/Write/Edit em .env, secrets, credenciais, chaves privadas
  2. Deny rules por comando — bloqueia rm -rf, git push --force, git reset --hard
  3. Hooks PreToolUse — scripts que leem o input da ferramenta via stdin, detectam secrets no conteúdo e bloqueiam comandos Bash que manipulam .env
  4. CLAUDE.md — regras semânticas que guiam o comportamento do Claude Code em tudo que ele escreve

Cada camada cobre o que a anterior não pega. Deny rules não analisam conteúdo. Hooks não definem comportamento. O CLAUDE.md não tem enforcement binário. Juntas, cobrem praticamente todos os vetores.

Segurança não é só auditar o código que o Claude Code analisa. É configurar o ambiente pra que ele nunca tenha acesso ao que não precisa. Princípio do menor privilégio. Vale pra humanos, vale pra agentes de IA.