DEV Community

Cover image for Como Criar Seu Próprio Código Claude?
Lucas
Lucas

Posted on • Originally published at apidog.com

Como Criar Seu Próprio Código Claude?

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.

Experimente o Apidog hoje

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Envia mensagens para a API do Claude (prompt de sistema + definições de ferramentas)
  2. Recebe uma resposta contendo texto e/ou blocos tool_use
  3. Executa cada ferramenta solicitada por meio de um mapa de despacho nome-para-manipulador
  4. Adiciona os resultados da ferramenta de volta à lista de mensagens
  5. Se a resposta contiver mais chamadas de ferramentas, volta para a etapa 1
  6. 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})
Enter fullscreen mode Exit fullscreen mode

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 BashTool bloqueia 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"]
        }
    }
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.

Captura de tela do Apidog mostrando detalhes da API Anthropic Messages

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:

  1. Abra o Apidog e crie um novo projeto para seu agente
  2. Importe o endpoint da API Anthropic Messages: POST https://api.anthropic.com/v1/messages
  3. Configure o corpo da requisição com seu prompt de sistema, array de ferramentas e mensagens
  4. 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 messages como 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()
Enter fullscreen mode Exit fullscreen mode

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)