Resumo
O vazamento do código-fonte do Claude expôs uma base de código TypeScript de 512.000 linhas em 31 de março de 2026. A arquitetura se resume a um loop while que chama a API do Claude, despacha chamadas de ferramentas e alimenta os resultados de volta. Você pode construir sua própria versão com Python, o SDK Anthropic e cerca de 200 linhas de código para o loop central. Este guia detalha cada componente e mostra como recriá-los.
Introdução
Em 31 de março de 2026, a Anthropic enviou um arquivo de mapa de origem de 59,8 MB dentro da versão 2.1.88 de seu pacote npm @anthropic-ai/claude-code. Mapas de origem são artefatos de depuração que revertem JavaScript minificado de volta ao código-fonte original. Como a ferramenta de construção da Anthropic (o bundler do Bun) os gera por padrão, toda a base de código TypeScript foi recuperável.
Em questão de horas, desenvolvedores espelharam o código em dezenas de repositórios GitHub. A comunidade rapidamente dissecou cada módulo, desde o loop do agente mestre até recursos ocultos como "modo disfarçado" e injeção de ferramenta falsa.
A reação foi dividida. Alguns criticaram as práticas de segurança da Anthropic. Outros ficaram fascinados pela arquitetura. Mas a resposta mais produtiva veio de desenvolvedores que perguntaram: "Posso construir isso sozinho?"
A resposta é sim. Os padrões centrais são simples. Este guia percorre cada camada arquitetônica, explica por que a Anthropic fez as escolhas que fez e fornece código funcional que você pode usar como ponto de partida. Você também aprenderá como testar as interações de API do seu agente personalizado com o Apidog, o que torna a depuração de conversas de API multi-turn muito mais fácil do que comandos curl brutos.
O que o vazamento revelou sobre a arquitetura do Claude Code
A base de código em um piscar de olhos
O Claude Code, internamente codinome "Tengu", abrange cerca de 1.900 arquivos. A organização do módulo se divide em camadas claras:
cli/ - Interface de Terminal (React + Ink)
tools/ - Mais de 40 implementações de ferramentas
core/ - Prompts de sistema, permissões, constantes
assistant/ - Orquestração de agente
services/ - Chamadas de API, compactação, OAuth, telemetria
O CLI em si é um aplicativo React renderizado via Ink, um renderizador React para saída de terminal. Ele usa Yoga (um motor flexbox CSS) para layout e códigos de escape ANSI para estilização. Cada visualização de conversação, área de entrada, exibição de chamada de ferramenta e caixa de diálogo de permissão é um componente React.
Isso é superprojetado para a maioria dos projetos DIY. Você não precisa de uma interface de terminal baseada em React para construir um agente de codificação funcional. Um loop REPL simples funciona bem.
O loop mestre do agente
Remova a interface do usuário, a telemetria e os sinalizadores de recursos, e o núcleo do Claude Code é um loop while. A Anthropic o chama internamente de "nO". Veja o que ele faz:
- Envia mensagens para a API do Claude (prompt de sistema + definições de ferramentas)
- Recebe uma resposta contendo texto e/ou blocos
tool_use - Executa cada ferramenta solicitada por meio de um mapa de despacho nome-para-manipulador
- Adiciona os resultados da ferramenta de volta à lista de mensagens
- Se a resposta contiver mais chamadas de ferramentas, volta para a etapa 1
- Se a resposta for texto simples sem chamadas de ferramentas, retorna para o usuário
Um "turn" é uma rodada completa. Os turnos continuam até que o Claude produza texto sem invocações de ferramentas. Esse é o padrão completo do agente.
Exemplo mínimo em Python:
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
def agent_loop(system_prompt: str, tools: list, messages: list) -> str:
"""The core agent loop - keep calling until no more tool use."""
while True:
response = client.messages.create(
model=MODEL,
max_tokens=16384,
system=system_prompt,
tools=tools,
messages=messages,
)
# Add assistant response to conversation
messages.append({"role": "assistant", "content": response.content})
# If the model stopped without requesting tools, we're done
if response.stop_reason != "tool_use":
# Extract the final text
return "".join(
block.text for block in response.content
if hasattr(block, "text")
)
# Execute each tool call and collect results
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
# Feed results back as a user message
messages.append({"role": "user", "content": tool_results})
O restante da complexidade do Claude Code vem das próprias ferramentas, do sistema de permissão, do gerenciamento de contexto e da memória.
Construindo o sistema de ferramentas
Por que ferramentas dedicadas superam um único comando bash
O Claude Code usa ferramentas dedicadas para operações de arquivo em vez de rotear tudo via bash.
Exemplo: existe uma ferramenta Read (não cat), uma ferramenta Edit (não sed), uma ferramenta Grep (não grep) e uma ferramenta Glob (não find). O prompt do sistema diz explicitamente ao modelo para preferir estas em vez de equivalentes bash.
Por quê?
- Saída estruturada: Ferramentas dedicadas retornam resultados em formatos consistentes, facilitando o parsing pelo modelo.
-
Segurança: A
BashToolbloqueia backticks e subshells para evitar injeção. - Eficiência de tokens: Os resultados podem ser truncados/amostrados — comandos bash podem despejar saída demais.
O conjunto de ferramentas essencial
Para um agente DIY, foque nestas cinco ferramentas:
TOOLS = [
{
"name": "read_file",
"description": "Read a file from the filesystem. Returns contents with line numbers.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Absolute path to the file"
},
"offset": {
"type": "integer",
"description": "Line number to start reading from (0-indexed)"
},
"limit": {
"type": "integer",
"description": "Max lines to read. Defaults to 2000."
}
},
"required": ["file_path"]
}
},
{
"name": "write_file",
"description": "Write content to a file. Creates the file if it doesn't exist.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Absolute path"},
"content": {"type": "string", "description": "File content to write"}
},
"required": ["file_path", "content"]
}
},
{
"name": "edit_file",
"description": "Replace a specific string in a file. The old_string must be unique.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Absolute path"},
"old_string": {"type": "string", "description": "Text to find"},
"new_string": {"type": "string", "description": "Replacement text"}
},
"required": ["file_path", "old_string", "new_string"]
}
},
{
"name": "run_command",
"description": "Execute a shell command and return stdout/stderr.",
"input_schema": {
"type": "object",
"properties": {
"command": {"type": "string", "description": "Shell command to run"},
"timeout": {"type": "integer", "description": "Timeout in seconds. Default 120."}
},
"required": ["command"]
}
},
{
"name": "search_code",
"description": "Search for a regex pattern across files in a directory.",
"input_schema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex pattern"},
"path": {"type": "string", "description": "Directory to search"},
"file_glob": {"type": "string", "description": "File pattern filter, e.g. '*.py'"}
},
"required": ["pattern"]
}
}
]
Despacho do manipulador de ferramentas
Mapeie cada nome de ferramenta para uma função:
import subprocess
import os
import re
def execute_tool(name: str, params: dict) -> str:
"""Dispatch tool calls to their handlers."""
handlers = {
"read_file": handle_read_file,
"write_file": handle_write_file,
"edit_file": handle_edit_file,
"run_command": handle_run_command,
"search_code": handle_search_code,
}
handler = handlers.get(name)
if not handler:
return f"Error: Unknown tool '{name}'"
try:
return handler(params)
except Exception as e:
return f"Error: {str(e)}"
def handle_read_file(params: dict) -> str:
path = params["file_path"]
offset = params.get("offset", 0)
limit = params.get("limit", 2000)
with open(path, "r") as f:
lines = f.readlines()
selected = lines[offset:offset + limit]
numbered = [f"{i + offset + 1}\t{line}" for i, line in enumerate(selected)]
return "".join(numbered)
def handle_write_file(params: dict) -> str:
path = params["file_path"]
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(params["content"])
return f"Successfully wrote to {path}"
def handle_edit_file(params: dict) -> str:
path = params["file_path"]
with open(path, "r") as f:
content = f.read()
old = params["old_string"]
if content.count(old) == 0:
return f"Error: '{old[:50]}...' not found in {path}"
if content.count(old) > 1:
return f"Error: '{old[:50]}...' matches {content.count(old)} locations. Be more specific."
new_content = content.replace(old, params["new_string"], 1)
with open(path, "w") as f:
f.write(new_content)
return f"Successfully edited {path}"
def handle_run_command(params: dict) -> str:
cmd = params["command"]
timeout = params.get("timeout", 120)
# Basic safety: block dangerous patterns
blocked = ["rm -rf /", "mkfs", "> /dev/"]
for pattern in blocked:
if pattern in cmd:
return f"Error: Blocked dangerous command pattern: {pattern}"
result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
timeout=timeout, cwd=os.getcwd()
)
output = ""
if result.stdout:
output += result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
if not output.strip():
output = f"Command completed with exit code {result.returncode}"
# Truncate large outputs to save context tokens
if len(output) > 30000:
output = output[:15000] + "\n\n... [truncated] ...\n\n" + output[-15000:]
return output
def handle_search_code(params: dict) -> str:
pattern = params["pattern"]
path = params.get("path", os.getcwd())
file_glob = params.get("file_glob", "")
cmd = ["grep", "-rn", "--include", file_glob, pattern, path] if file_glob else \
["grep", "-rn", pattern, path]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if not result.stdout.strip():
return f"No matches found for pattern: {pattern}"
lines = result.stdout.strip().split("\n")
if len(lines) > 50:
return "\n".join(lines[:50]) + f"\n\n... ({len(lines) - 50} more matches)"
return result.stdout
Gerenciamento de contexto: o problema difícil
Por que o contexto importa mais do que a engenharia de prompt
O código-fonte vazado mostra que o Claude Code dedica mais engenharia ao gerenciamento de contexto do que ao prompt. O compressor de contexto ("wU2") tem cinco estratégias.
Para DIY, foque em duas:
- Auto-compactação: acione quando a conversa atingir ~92% da janela de contexto, reservando tokens para o resumo.
- Reinjeção de CLAUDE.md: injete as diretrizes do projeto a cada turno.
Construindo um compressor simples
def maybe_compact(messages: list, system_prompt: str, max_tokens: int = 180000) -> list:
"""Compact conversation when it gets too long."""
# Rough estimate: 4 chars per token
total_chars = sum(
len(str(m.get("content", ""))) for m in messages
)
estimated_tokens = total_chars // 4
if estimated_tokens < max_tokens * 0.85:
return messages # Not yet at the limit
# Ask the model to summarize the conversation so far
summary_response = client.messages.create(
model=MODEL,
max_tokens=4096,
system="Summarize this conversation. Keep all file paths, decisions made, errors encountered, and current task state. Be specific about what was changed and why.",
messages=messages,
)
summary_text = summary_response.content[0].text
# Replace conversation with summary + recent messages
compacted = [
{"role": "user", "content": f"[Conversation summary]\n{summary_text}"},
{"role": "assistant", "content": "I have the context from our previous conversation. What should I work on next?"},
]
# Keep the last 4 messages for immediate context
compacted.extend(messages[-4:])
return compacted
Re-injetando contexto do projeto
Reinjete sempre .claude/CLAUDE.md e CLAUDE.md a cada turno:
def build_system_prompt(project_dir: str) -> str:
"""Build system prompt with project context re-injection."""
base_prompt = """You are a coding assistant that helps with software engineering tasks.
You have access to tools for reading, writing, editing files, running commands, and searching code.
Always read files before modifying them. Prefer edit_file over write_file for existing files.
Keep responses concise. Focus on the code, not explanations."""
# Look for project guidelines
claude_md_path = os.path.join(project_dir, ".claude", "CLAUDE.md")
if os.path.exists(claude_md_path):
with open(claude_md_path, "r") as f:
project_context = f.read()
base_prompt += f"\n\n# Project guidelines\n{project_context}"
# Also check for a root CLAUDE.md
root_md = os.path.join(project_dir, "CLAUDE.md")
if os.path.exists(root_md):
with open(root_md, "r") as f:
root_context = f.read()
base_prompt += f"\n\n# Repository guidelines\n{root_context}"
return base_prompt
O sistema de memória de três camadas
O Claude Code usa três níveis de memória:
Camada 1: MEMORY.md (sempre carregado)
Índice leve, sempre presente no prompt:
- [User preferences](memory/user-prefs.md) - prefers TypeScript, uses Vim keybindings
- [API conventions](memory/api-conventions.md) - REST with JSON:API spec, snake_case
- [Deploy process](memory/deploy.md) - uses GitHub Actions, deploys to AWS EKS
Camada 2: arquivos de tópico (carregados sob demanda)
Arquivos detalhados, carregados somente quando relevantes.
Camada 3: transcrições de sessão (pesquisadas, nunca lidas)
Logs completos, somente pesquisados por identificador — nunca carregados inteiros.
Construindo um sistema de memória mínimo
import json
MEMORY_DIR = ".agent/memory"
def load_memory_index() -> str:
"""Load the memory index for system prompt injection."""
index_path = os.path.join(MEMORY_DIR, "MEMORY.md")
if os.path.exists(index_path):
with open(index_path, "r") as f:
return f.read()
return ""
def save_memory(key: str, content: str, description: str):
"""Save a memory entry and update the index."""
os.makedirs(MEMORY_DIR, exist_ok=True)
# Write the memory file
filename = f"{key.replace(' ', '-').lower()}.md"
filepath = os.path.join(MEMORY_DIR, filename)
with open(filepath, "w") as f:
f.write(f"---\nname: {key}\ndescription: {description}\n---\n\n{content}")
# Update the index
index_path = os.path.join(MEMORY_DIR, "MEMORY.md")
index_lines = []
if os.path.exists(index_path):
with open(index_path, "r") as f:
index_lines = f.readlines()
# Add or update entry
new_entry = f"- [{key}]({filename}) - {description}\n"
updated = False
for i, line in enumerate(index_lines):
if filename in line:
index_lines[i] = new_entry
updated = True
break
if not updated:
index_lines.append(new_entry)
with open(index_path, "w") as f:
f.writelines(index_lines)
Adicione uma ferramenta save_memory à sua lista para persistir conhecimento entre sessões.
Adicionando um sistema de permissões
O Claude Code revela cinco modos de permissão. Para DIY, um sistema simples de três níveis cobre a maioria dos casos:
# Risk levels for operations
RISK_LEVELS = {
"read_file": "low",
"search_code": "low",
"edit_file": "medium",
"write_file": "medium",
"run_command": "high",
}
def check_permission(tool_name: str, params: dict, auto_approve_low: bool = True) -> bool:
"""Check if the user approves this tool call."""
risk = RISK_LEVELS.get(tool_name, "high")
if risk == "low" and auto_approve_low:
return True
# Show the user what's about to happen
print(f"\n--- Permission check ({risk.upper()} risk) ---")
print(f"Tool: {tool_name}")
for key, value in params.items():
display = str(value)[:200]
print(f" {key}: {display}")
response = input("Allow? [y/n/always]: ").strip().lower()
if response == "always":
RISK_LEVELS[tool_name] = "low" # Auto-approve this tool going forward
return True
return response == "y"
Testando as chamadas de API do seu agente com o Apidog
Construir um agente de codificação significa fazer centenas de chamadas de API para o Claude. Depurar essas interações, especialmente conversas multi-turn com uso de ferramentas, é doloroso com logs brutos.
Apidog ajuda você a inspecionar e testar as requisições exatas de API que seu agente envia. Veja como usá-lo durante o desenvolvimento:
Capture e repita requisições de API
Configure o Apidog como um proxy para interceptar as chamadas do seu agente para a API Anthropic:
- Abra o Apidog e crie um novo projeto para seu agente
- Importe o endpoint da API Anthropic Messages:
POST https://api.anthropic.com/v1/messages - Configure o corpo da requisição com seu prompt de sistema, array de ferramentas e mensagens
- Teste turnos individuais repetindo requisições capturadas com parâmetros modificados
Assim, você pode isolar turnos específicos de uso de ferramentas sem executar o loop completo do agente. Quando o modelo retorna uma chamada de ferramenta inesperada ou um parâmetro alucinado, basta modificar o corpo da requisição no editor visual do Apidog e reenviá-lo.
Depure conversas multi-turn
Para reproduzir um estado de conversa:
- Salve o array completo de
messagescomo variável de ambiente após cada turno - Repita a partir de qualquer ponto
- Compare os resultados das ferramentas entre execuções para identificar divergências
Valide esquemas de ferramentas
Implemente seus esquemas no Apidog e use o validador de Esquema JSON para identificar problemas antes de enviar para a API.
Juntando tudo: o REPL completo
Aqui está um agente completo, conectado como um REPL funcional:
#!/usr/bin/env python3
"""A minimal Claude Code-style coding agent."""
import anthropic
import os
import sys
client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"
PROJECT_DIR = os.getcwd()
def main():
system_prompt = build_system_prompt(PROJECT_DIR)
memory = load_memory_index()
if memory:
system_prompt += f"\n\n# Memory\n{memory}"
messages = []
print("Coding agent ready. Type 'quit' to exit.\n")
while True:
user_input = input("> ").strip()
if user_input.lower() in ("quit", "exit"):
break
if not user_input:
continue
messages.append({"role": "user", "content": user_input})
# Compact if needed
messages = maybe_compact(messages, system_prompt)
# Re-inject project context (Claude Code does this every turn)
current_system = build_system_prompt(PROJECT_DIR)
memory = load_memory_index()
if memory:
current_system += f"\n\n# Memory\n{memory}"
# Run the agent loop
result = agent_loop(current_system, TOOLS, messages)
print(f"\n{result}\n")
if __name__ == "__main__":
main()
Com menos de 300 linhas de Python, você terá um agente de codificação funcional: lê arquivos, edita código, executa comandos, pesquisa bases de código, gerencia contexto e memória entre sessões.
O que adicionar a seguir
O vazamento revela vários recursos úteis:
Subagentes para trabalho paralelo
Gere subagentes (novos agent_loop()) para tarefas independentes e retorne o resultado como string. Isso evita poluir a conversa principal.
Deduplicação de leitura de arquivos
Rastreie arquivos lidos e seus tempos de modificação. Se um arquivo não mudou, informe ao modelo e evite releitura.
Truncamento e amostragem de saída
Trunque saídas massivas de ferramentas (grep, por exemplo) e informe quantos resultados foram omitidos — economize tokens.
Autocompactação com reinjeção de arquivos
Após resumir a conversa, reinjete o conteúdo dos arquivos acessados recentemente (até 5.000 tokens por arquivo) para manter o contexto de trabalho.
O que aprendemos com o vazamento
O Claude Code confirmou vários padrões de agentes de IA:
- O loop central é simples: O padrão do agente cabe em 30 linhas. A complexidade está nas ferramentas e contexto.
- Ferramentas dedicadas superam o bash: Estrutura e densidade de informação melhores.
- A memória precisa de camadas: Índice sempre carregado, arquivos detalhados sob demanda, logs pesquisáveis.
- O arcabouço é o produto: O modelo traz inteligência; o arcabouço traz percepção, ação e memória.
Se quiser testar e depurar as interações de API do seu agente personalizado, incluindo conversas multi-turn com uso de ferramentas, esquemas de solicitação complexos e validação de resposta, experimente o Apidog gratuitamente. Ele cuida da depuração da API para que você foque na lógica do agente.
Perguntas Frequentes
Posso usar legalmente padrões do vazamento do Claude Code?
O vazamento expôs padrões arquitetônicos, não algoritmos proprietários. Construir um agente de codificação que usa um loop while com despacho de ferramentas é padrão documentado pela própria Anthropic. Não copie código literal, mas recriar a arquitetura com seu próprio código é prática comum.
Qual modelo devo usar para um agente de codificação DIY?
Claude Sonnet 4.6 equilibra velocidade e capacidade para tarefas de codificação. Claude Opus 4.6 é melhor para decisões complexas, mas mais caro. Para edições simples, Claude Haiku 4.5 funciona e custa 90% menos.
Quanto custa para rodar seu próprio agente de codificação?
Uma sessão típica (30-50 turnos) com Claude Sonnet 4.6 custa de $1-5 em taxas de API. O principal fator é o tamanho da janela de contexto; compactação agressiva mantém custos baixos.
Por que o Claude Code usa React para um aplicativo de terminal?
Ink (React para terminais) permite reutilizar componentes React e gerenciar estados complexos da UI. Para DIY, um simples REPL input()/print() já basta.
Qual é o recurso mais importante a ser construído após o loop principal?
O sistema de permissões. Sem ele, o modelo pode sobrescrever arquivos e executar comandos arbitrários sem supervisão. Uma simples confirmação já evita muitos problemas.
Como o Claude Code lida com erros de chamadas de ferramentas?
Os erros são retornados como texto em mensagens tool_result. O modelo decide se tenta novamente, ajusta a abordagem ou pergunta ao usuário. Não há tratamento de erro especial — o raciocínio do modelo cuida da recuperação.
Posso usar isso com modelos diferentes do Claude?
Sim. O padrão de uso de ferramentas funciona com qualquer modelo que suporte chamadas de função: GPT-4, Gemini, Llama, etc. Adapte o formato da chamada da API, mas o loop, ferramentas e memória são agnósticos ao modelo.
Como evito que o agente execute comandos perigosos?
Implemente uma lista negra (rm -rf /, mkfs, etc.) e exija aprovação explícita para run_command. Classifique cada operação como risco BAIXO, MÉDIO ou ALTO e bloqueie ou peça permissão de acordo.

Top comments (0)