Blog · AI Harness

CLAUDE.md: o que é e como escrever um que o agente segue (exemplo, tamanho ideal e CLAUDE.md vs AGENTS.md)

Capiva · Outubro 2026 · 12 min de leitura

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
> /init

O /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

ArquivoPra quêVai pro git?
./CLAUDE.md ou ./.claude/CLAUDE.mdRegras do projeto, pro time todoSim
./CLAUDE.local.mdSuas preferências naquele projetoNão (ponha no .gitignore)
~/.claude/CLAUDE.mdSuas preferências em todos os projetosNão
.claude/rules/*.mdRegras de uma parte do códigoSim
CLAUDE.md de subdiretórioRegras daquela pasta, carregadas sob demandaSim

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órioO que o Claude Code lê
Só AGENTS.mdO AGENTS.md
AGENTS.md e um CLAUDE.md ou CLAUDE.local.md na pasta atual ou acimaSó os CLAUDE.md
Um CLAUDE.md que importa @AGENTS.mdOs 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.md

O 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 /doctor do 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/rules com paths.

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 0

Um 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 destaca

O 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.md parou 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/rules nunca carrega. O glob em paths não casa. Use o log de InstructionsLoaded.
  • [ ] 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

Quer um CLAUDE.md enxuto e os hooks que sustentam ele já montados? Conheça o capivaOS.