O CLAUDE.md é um arquivo Markdown que o Claude Code lê no início de cada sessão e injeta no contexto como instrução persistente do projeto. Ele não é configuração aplicada: segundo a documentação de memória, o conteúdo entra como uma mensagem de usuário depois do system prompt, e "there's no guarantee of strict compliance, especially for vague or conflicting instructions". Escrever um CLAUDE.md que funciona é, então, escrever instruções curtas, concretas e verificáveis, e tirar dele tudo que precisa ser garantido.
Este post é um dos guias práticos do guia de Claude Code, que explica o papel do CLAUDE.md dentro do harness (ele é um guide, o hook é um sensor). Aqui vamos escrever um, linha por linha, e montar um script que revisa o arquivo no CI.
Gere o rascunho com /init e apague metade
cd seu-projeto
claude
> /initO /init analisa o repositório e gera um CLAUDE.md com comandos de build, testes e convenções que ele encontrou. Se o arquivo já existe, ele sugere melhorias em vez de sobrescrever. Com CLAUDE_CODE_NEW_INIT=1 no ambiente, o fluxo fica interativo: pergunta se você quer CLAUDE.md, skills e hooks e mostra a proposta antes de gravar.
Trate o rascunho como ponto de partida. O que o /init descobre sozinho lendo o código, o Claude também descobre sozinho lendo o código. O valor do arquivo está no que não dá pra inferir. A pergunta que a página de boas práticas manda fazer pra cada linha é: "Would removing this cause Claude to make mistakes?". Se a resposta é não, corte.
Escolha o arquivo certo
| Arquivo | Pra quê | Vai pro git? |
|---|---|---|
./CLAUDE.md ou ./.claude/CLAUDE.md | Regras do projeto, pro time todo | Sim |
./CLAUDE.local.md | Suas preferências naquele projeto | Não (ponha no .gitignore) |
~/.claude/CLAUDE.md | Suas preferências em todos os projetos | Não |
.claude/rules/*.md | Regras de uma parte do código | Sim |
CLAUDE.md de subdiretório | Regras daquela pasta, carregadas sob demanda | Sim |
Os arquivos se somam, não se sobrescrevem. O Claude Code sobe da pasta atual até a raiz e concatena o que encontra. Arquivos de subdiretório só entram quando o Claude lê algo naquela pasta.
Decida entre CLAUDE.md e AGENTS.md
O AGENTS.md é o arquivo de instruções que outros agentes de código leem. O Claude Code também lê, a partir da v2.1.277, com esta regra de precedência:
| O que existe no repositório | O que o Claude Code lê |
|---|---|
Só AGENTS.md | O AGENTS.md |
AGENTS.md e um CLAUDE.md ou CLAUDE.local.md na pasta atual ou acima | Só os CLAUDE.md |
Um CLAUDE.md que importa @AGENTS.md | Os dois, via import |
Ou seja: criar um CLAUDE.local.md pessoal num projeto que vive de AGENTS.md faz o Claude parar de ler o AGENTS.md pra você. Se o time usa vários agentes, o arranjo mais simples é um CLAUDE.md curto assim:
@AGENTS.md
## Só para o Claude Code
- Regras de migrations ficam em .claude/rules/migrations.mdO AGENTS.md continua sendo a fonte única, e o CLAUDE.md acrescenta só o que é específico do Claude Code. Quem prefere não usar import pode trocar a configuração Project instructions para claude-md-and-agents-md, que carrega os dois.
Escreva só o que o agente não adivinha
Este é o CLAUDE.md de exemplo do repositório deste post, pra uma API FastAPI fictícia:
# Projeto: api-pedidos (FastAPI + Postgres)
Visão geral e setup local: @README.md
## Comandos
- Subir dependências: `make up` (Postgres em Docker, porta 5433, não 5432)
- Testes de um arquivo: `make test ARQ=tests/test_pedidos.py`; a suíte inteira leva ~4 min, rode só no fim
- Lint e tipos: `make check` (ruff + mypy). Rode antes de dizer que terminou
## Convenções que fogem do padrão
- Valores monetários são `int` em centavos. Nunca `float`, nunca `Decimal` na borda da API
- Datas no banco em UTC; conversão para America/Sao_Paulo só na camada de resposta
- Erros de domínio herdam de `app.erros.ErroDeDominio`; não lance `HTTPException` fora de `app/api/`
## Armadilhas conhecidas
- `tests/fixtures/` é gerado por `make fixtures`. Não edite à mão
- O cliente do gateway de pagamento tem modo sandbox via `PAGAMENTO_SANDBOX=1`; sem ela, os testes de integração batem na API real
## Fluxo
- Branch: `feat/<ticket>-descricao-curta`. Commits em português, no imperativo
- Mudou contrato da API? Atualize `docs/openapi-notas.md` no mesmo PR
<!-- Regras sobre migrations ficam em .claude/rules/migrations.md (carrega só quando o Claude mexe em migrations/) -->O que tem aqui e o que ficou de fora, seguindo a tabela de incluir e excluir da documentação:
- Comandos que o Claude não adivinha. A porta 5433, o parâmetro
ARQ=, os 4 minutos da suíte. Cada um evita um erro concreto. - Convenções que fogem do padrão. Centavos em
inté decisão do projeto, não convenção da linguagem. "Use type hints" ficou de fora: o Claude já faz isso. - Armadilhas. Arquivo gerado que parece editável, variável de ambiente que muda o comportamento dos testes.
- Nada de tour pelo código. Descrição pasta por pasta, lista de dependências e documentação de API ficam fora. O Claude lê o código. O
/doctordo Claude Code propõe cortes justamente nesse tipo de conteúdo.
O comentário HTML no fim não gasta token: o Claude Code remove comentários HTML em bloco antes de o conteúdo entrar no contexto. Use pra deixar nota pra humanos.
Cada instrução é verificável. "Rode make check antes de dizer que terminou" dá pra conferir no transcript. "Escreva código limpo" não dá, e a documentação cita exatamente esse exemplo como o que não incluir.
Mova o que é de uma pasta só para .claude/rules
A meta da documentação é menos de 200 linhas por arquivo. O jeito de crescer sem estourar é tirar do arquivo principal o que só importa em parte do código:
---
paths:
- "migrations/**/*.py"
---
# Migrations
- Gere com `make migration MSG="descricao"`; não escreva o cabeçalho à mão
- Toda migration precisa de `downgrade()` funcional
- Migration já versionada é imutável: crie uma nova (um hook PreToolUse bloqueia a edição)Salvo em .claude/rules/migrations.md, esse arquivo só entra no contexto quando o Claude lê ou edita um arquivo que casa com o glob. paths é o único campo de frontmatter que o Claude Code lê numa regra; qualquer outro é ignorado sem erro. Uma regra sem paths carrega no início, como se estivesse no CLAUDE.md.
Repare na última linha: a regra explica o porquê, mas quem garante é o hook de PreToolUse. A própria documentação de memória diz: pra bloquear uma ação independentemente do que o Claude decidir, use um hook.
Use imports com @ sem cair nas pegadinhas
@README.md no exemplo puxa o README inteiro pro contexto. As regras, todas da documentação:
- O caminho é relativo ao arquivo que importa, não à pasta atual.
- Imports podem importar outros, até quatro níveis.
@dentro de code span ou bloco de código não importa. Pra citar um caminho sem carregar, escreva entre crases.- Na primeira vez que um projeto importa um arquivo de fora dele, o Claude Code pede aprovação.
- Import não economiza contexto. O arquivo importado carrega no início junto com o resto. Import organiza; quem economiza é
.claude/rulescompaths.
Confira o que foi carregado, e quando
Dentro da sessão, /context lista os arquivos em Memory files e /memory abre cada um. Pra ver regras sob demanda entrando no meio da sessão, registre um hook no evento InstructionsLoaded:
#!/usr/bin/env python3
"""InstructionsLoaded: registra quais arquivos de instrução entraram no contexto, e por quê."""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
e = json.load(sys.stdin)
linha = {
"quando": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"arquivo": e.get("file_path"),
"motivo": e.get(
"load_reason"
), # session_start, nested_traversal, path_glob_match, include, compact
"gatilho": e.get("trigger_file_path"),
}
log = Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")) / ".claude" / "instrucoes.log"
with log.open("a", encoding="utf-8") as f:
f.write(json.dumps(linha, ensure_ascii=False) + "\n"){
"hooks": {
"InstructionsLoaded": [
{
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/log_instrucoes.py"]
}
]
}
]
}
}Com a regra de migrations, uma linha do log fica assim (exemplo no formato que o hook grava; essa linha não foi capturada, porque a regra só carrega quando o Claude lê um arquivo de migrations/, e isso exige uma sessão logada):
{"quando": "2026-10-11T23:04:22+00:00", "arquivo": "/p/.claude/rules/migrations.md", "motivo": "path_glob_match", "gatilho": "/p/migrations/0002.py"}Se a regra que você esperava nunca aparece com path_glob_match, o glob está errado.
O hook dispara antes de qualquer chamada ao modelo, então dá pra testar sem conta. Na pasta de exemplo, com Claude Code 2.1.296 e sem login, claude -p "oi" termina com Not logged in · Please run /login, mas o InstructionsLoaded já rodou duas vezes (o CLAUDE.md importa o README com @):
{"quando": "2026-10-12T17:38:15+00:00", "arquivo": "/tmp/cm/proj/README.md", "motivo": "include", "gatilho": null}
{"quando": "2026-10-12T17:38:15+00:00", "arquivo": "/tmp/cm/proj/CLAUDE.md", "motivo": "session_start", "gatilho": null}E o log de debug (--debug-file) confirma o exec form:
[DEBUG] InstructionsLoaded:include [python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/log_instrucoes.py] completed with status 0
[DEBUG] InstructionsLoaded:session_start [python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/log_instrucoes.py] completed with status 0Um cuidado: sem --bare, claude -p roda os hooks do .claude/settings.json do projeto mesmo numa pasta que você nunca marcou como confiável, segundo a documentação do modo headless. Não rode claude -p em repositório de terceiros sem ler os hooks antes.
Revise o CLAUDE.md no CI
O arquivo fica desatualizado como qualquer código: o script test:unit é renomeado, o docs/arquitetura.md some, e o CLAUDE.md continua mandando o agente usar os dois. Este script pega os erros mecânicos:
#!/usr/bin/env python3
"""Revisa um CLAUDE.md contra as recomendações da documentação do Claude Code.
Uso: python3 claude_md_lint.py [CAMINHO_DO_CLAUDE.md]
Sai com 1 se houver erro (bom para pre-commit ou CI).
"""
import json
import re
import sys
from pathlib import Path
LIMITE_LINHAS = 200 # meta da doc: menos de 200 linhas por arquivo
VAGAS = [
"código limpo",
"boas práticas",
"seja cuidadoso",
"formate corretamente",
"clean code",
"best practices",
"format properly",
"be careful",
]
IMPORT = re.compile(r"(?<![\w`])@((?:[^\s`\\]|\\ )+)")
COMANDO = re.compile(r"`((?:npm run|pnpm|yarn|make) [\w:.-]+)`")
def sem_codigo(texto: str) -> str:
"""Remove blocos cercados e code spans: a doc diz que @imports ali não valem."""
texto = re.sub(r"```.*?```", "", texto, flags=re.DOTALL)
return re.sub(r"`[^`\n]*`", "", texto)
def scripts_do_projeto(raiz: Path) -> set[str]:
nomes = set()
pkg = raiz / "package.json"
if pkg.exists():
nomes |= set(json.loads(pkg.read_text()).get("scripts", {}))
mk = raiz / "Makefile"
if mk.exists():
nomes |= set(re.findall(r"^([\w.-]+):", mk.read_text(), flags=re.MULTILINE))
return nomes
def revisar(arquivo: Path) -> list[tuple[str, str]]:
texto = arquivo.read_text(encoding="utf-8")
achados = []
n = texto.count("\n") + 1
if n > LIMITE_LINHAS:
achados.append(
("aviso", f"{n} linhas; a doc recomenda menos de {LIMITE_LINHAS}")
)
for m in IMPORT.finditer(sem_codigo(texto)):
bruto = m.group(1).rstrip(".,;:)")
alvo = Path(bruto.replace("\\ ", " ")).expanduser()
if not alvo.is_absolute():
alvo = arquivo.parent / alvo
if not alvo.exists():
achados.append(("erro", f"import @{bruto} aponta para arquivo inexistente"))
scripts = scripts_do_projeto(arquivo.parent)
for cmd in COMANDO.findall(texto):
nome = cmd.split()[-1]
if scripts and nome not in scripts:
achados.append(
(
"erro",
f"`{cmd}` citado, mas '{nome}' não existe em package.json/Makefile",
)
)
baixo = texto.lower()
for frase in VAGAS:
if frase in baixo:
achados.append(
("aviso", f"instrução vaga: '{frase}'. Troque por algo verificável")
)
enfase = len(re.findall(r"\b(IMPORTANT|IMPORTANTE|NUNCA|SEMPRE)\b", texto))
if enfase > 3:
achados.append(
(
"aviso",
f"{enfase} marcações de ênfase em caixa alta; se tudo é importante, nada se destaca",
)
)
return achados
if __name__ == "__main__":
alvo = Path(sys.argv[1] if len(sys.argv) > 1 else "CLAUDE.md")
achados = revisar(alvo)
for nivel, msg in achados:
print(f"{alvo}: {nivel}: {msg}")
if not achados:
print(f"{alvo}: ok")
sys.exit(1 if any(n == "erro" for n, _ in achados) else 0)Rodando contra um CLAUDE.md ruim de propósito (frases vagas, quatro linhas em caixa alta, imports quebrados, um script que não existe no package.json):
$ python3 claude_md_lint.py exemplo-ruim/CLAUDE.md
exemplo-ruim/CLAUDE.md: erro: import @docs/arquitetura.md aponta para arquivo inexistente
exemplo-ruim/CLAUDE.md: erro: import @CONTRIBUTING.md aponta para arquivo inexistente
exemplo-ruim/CLAUDE.md: erro: `npm run test:unit` citado, mas 'test:unit' não existe em package.json/Makefile
exemplo-ruim/CLAUDE.md: aviso: instrução vaga: 'código limpo'. Troque por algo verificável
exemplo-ruim/CLAUDE.md: aviso: instrução vaga: 'boas práticas'. Troque por algo verificável
exemplo-ruim/CLAUDE.md: aviso: instrução vaga: 'seja cuidadoso'. Troque por algo verificável
exemplo-ruim/CLAUDE.md: aviso: 4 marcações de ênfase em caixa alta; se tudo é importante, nada se destacaO aviso de ênfase vem direto da documentação: se uma instrução continua sendo ignorada, ponha "IMPORTANT" naquela linha, só nela. "If you emphasize many lines, none of them stands out." O script ignora @ entre crases, como o Claude Code faz.
O lint é mecânico de propósito. Pra revisão semântica (instrução contraditória, escrita pra modelo antigo), o Claude Code tem o /doctor prompt-audit, a partir da v2.1.283. Ele gera um relatório e só altera os arquivos se você pedir.
Como usamos na Capiva
No capivaOS, o CLAUDE.md passou de ~7,1 mil tokens (518 linhas) pra ~2,8 mil tokens (170 linhas), 61% a menos, e a camada sempre carregada inteira caiu 36% (de ~26,4 mil pra ~16,9 mil tokens). Os números são estimativas por caracteres/4 e estão no ADR-0011. O que permitiu o corte foi mover as regras críticas pra um hook de PreToolUse. A frase do ADR resume o critério: "Prompt text that merely restates what a hook mechanically denies is redundancy, not enforcement."
Checklist de troubleshooting
- [ ] O Claude ignora uma regra. O arquivo está longo e a regra se perdeu. Corte, ou mova pra
.claude/rules. Se a regra precisa valer sempre, transforme em hook. - [ ] O arquivo não aparece em
/context. Confira o nome (CLAUDE.md, maiúsculo) e a pasta. Arquivo de subdiretório só carrega quando o Claude lê algo lá. - [ ] O
AGENTS.mdparou de ser lido. Basta existir um CLAUDE.md ou CLAUDE.local.md na pasta atual ou acima. Importe com@AGENTS.md. - [ ] Um procedimento enorme no arquivo. Mova pra uma skill.
- [ ] Uma regra de
.claude/rulesnunca carrega. O glob empathsnão casa. Use o log deInstructionsLoaded. - [ ] Um import não funciona. Está entre crases, o caminho está relativo à pasta errada, ou passou de quatro níveis.
- [ ] Uma instrução sumiu depois do
/compact. O CLAUDE.md da raiz do projeto é relido do disco após a compactação. Instrução dada só na conversa se perde. - [ ] O Claude pergunta coisas que estão no arquivo. A frase está ambígua. Reescreva como comando verificável.
Perguntas frequentes
Qual o tamanho ideal de um CLAUDE.md?▼
A documentação recomenda menos de 200 linhas por arquivo. Mais que isso consome contexto e reduz a aderência.
Procedimento longo, tipo "como fazer deploy", vai no CLAUDE.md?▼
Não. Procedimento vira skill, que só carrega quando é usada. Veja Como criar uma Skill no Claude Code.
Posso pôr segredos ou variáveis de ambiente no CLAUDE.md?▼
Não. Ele vai pro git e pro contexto do modelo. Diga só o nome da variável, como PAGAMENTO_SANDBOX=1 no exemplo.
Vale escrever em português?▼
Vale. O exemplo deste post é todo em português. O que importa é ser concreto.
Leia também
- Claude Code é um harness: CLAUDE.md, hooks, MCP e subagentes (o guia principal)
- Como configurar hooks no Claude Code
- Como criar uma Skill no Claude Code
- Como instalar o Claude Code no Windows
- Como criar um servidor MCP em Python e configurar no Claude Code
- Como criar um llms.txt, o primo do CLAUDE.md voltado pra sites
- Harness engineering
Quer um CLAUDE.md enxuto e os hooks que sustentam ele já montados? Conheça o capivaOS.