DEV Community

Cover image for Comment créer votre propre code Claude ?
Antoine Laurent
Antoine Laurent

Posted on • Originally published at apidog.com

Comment créer votre propre code Claude ?

En bref

La fuite du code source de Claude Code a exposé une base de code TypeScript de 512 000 lignes le 31 mars 2026. L'architecture se résume à une boucle while qui appelle l'API Claude, distribue les appels d'outils et renvoie les résultats. Vous pouvez créer votre propre version avec Python, le SDK Anthropic et environ 200 lignes de code pour la boucle centrale. Ce guide détaille chaque composant et vous montre comment les recréer.

Essayez Apidog dès aujourd'hui

Introduction

Le 31 mars 2026, Anthropic a publié un fichier de carte source de 59,8 Mo dans la version 2.1.88 de son package npm @anthropic-ai/claude-code. Les cartes sources sont des artefacts de débogage qui permettent de reconstituer le code JavaScript minifié à partir de la source originale. Étant donné que l'outil de build d'Anthropic (le bundler de Bun) les génère par défaut, l'intégralité de la base de code TypeScript était récupérable.

En quelques heures, les développeurs avaient dupliqué le code sur des dizaines de dépôts GitHub. La communauté a rapidement disséqué chaque module, de la boucle de l'agent maître aux fonctionnalités cachées comme le « mode furtif » et l'injection d'outils factices.

Les réactions ont été mitigées. Certains ont critiqué les pratiques de sécurité d'Anthropic. D'autres ont été fascinés par l'architecture. Mais la réponse la plus productive est venue des développeurs qui ont demandé : « Puis-je construire cela moi-même ? »

La réponse est oui. Les modèles de base sont simples. Ce guide parcourt chaque couche architecturale, explique pourquoi Anthropic a fait les choix qu'il a faits et fournit du code fonctionnel que vous pouvez utiliser comme point de départ. Vous apprendrez également à tester les interactions API de votre agent personnalisé avec Apidog, ce qui rend le débogage des conversations API multi-tours beaucoup plus facile que les commandes curl brutes.

Ce que la fuite a révélé sur l'architecture de Claude Code

La base de code en un coup d'œil

Claude Code, dont le nom de code interne est « Tengu », s'étend sur environ 1 900 fichiers. L'organisation des modules se décompose en couches claires :

cli/          - Interface utilisateur en terminal (React + Ink)
tools/        - Plus de 40 implémentations d'outils
core/         - Invites système, permissions, constantes
assistant/    - Orchestration de l'agent
services/     - Appels API, compactage, OAuth, télémétrie
Enter fullscreen mode Exit fullscreen mode

L'interface CLI elle-même est une application React rendue via Ink, un moteur de rendu React pour la sortie terminale. Elle utilise Yoga (un moteur flexbox CSS) pour la mise en page et les codes d'échappement ANSI pour le style. Chaque vue de conversation, zone de saisie, affichage d'appel d'outil et boîte de dialogue d'autorisation est un composant React.

Pour vos propres agents, une interface REPL suffit largement.

La boucle de l'agent maître

En retirant l'UI, la télémétrie et les indicateurs de fonctionnalités, le cœur de Claude Code est une boucle while interne appelée « nO ». Elle fonctionne ainsi :

  1. Envoie des messages à l'API Claude (invite système + outils)
  2. Récupère la réponse contenant du texte et/ou des blocs tool_use
  3. Exécute chaque outil demandé via une carte de distribution nom-vers-gestionnaire
  4. Ajoute les résultats à la conversation
  5. Si d'autres appels d'outils sont demandés, recommence
  6. Si la réponse est du texte pur, retourne à l'utilisateur

Voici une version Python minimaliste :

import anthropic

client = anthropic.Anthropic()
MODEL = "claude-sonnet-4-6"

def agent_loop(system_prompt: str, tools: list, messages: list) -> str:
    """Boucle centrale de l'agent, continue jusqu'à ce qu'il n'y ait plus d'utilisation d'outils."""
    while True:
        response = client.messages.create(
            model=MODEL,
            max_tokens=16384,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return "".join(
                block.text for block in response.content
                if hasattr(block, "text")
            )

        # Exécuter chaque appel d'outil et collecter les résultats
        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,
                })
        messages.append({"role": "user", "content": tool_results})
Enter fullscreen mode Exit fullscreen mode

La majorité de la complexité réside dans la gestion des outils et du contexte.

Construire le système d'outils

Pourquoi des outils dédiés au lieu du bash ?

Claude Code opte systématiquement pour des outils dédiés : Read, Edit, Grep, Glob à la place de cat, sed, grep, find bash.

Les raisons :

  • Sortie structurée : Les outils dédiés renvoient un format prévisible.
  • Sécurité : Le BashTool bloque les motifs dangereux. Les outils dédiés éliminent ce risque.
  • Efficacité des jetons : Les résultats sont tronqués ou échantillonnés, contrairement aux sorties brutes bash.

L'ensemble d'outils essentiel

Pour un agent DIY, cinq outils suffisent. Exemple de définition :

TOOLS = [
    {
        "name": "read_file",
        "description": "Lit un fichier du système de fichiers. Renvoie le contenu avec les numéros de ligne.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string", "description": "Chemin absolu du fichier"},
                "offset": {"type": "integer", "description": "Numéro de ligne de départ"},
                "limit": {"type": "integer", "description": "Nombre maximum de lignes"}
            },
            "required": ["file_path"]
        }
    },
    {
        "name": "write_file",
        "description": "Écrit du contenu dans un fichier. Crée le fichier s'il n'existe pas.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string"},
                "content": {"type": "string"}
            },
            "required": ["file_path", "content"]
        }
    },
    {
        "name": "edit_file",
        "description": "Remplace une chaîne spécifique dans un fichier.",
        "input_schema": {
            "type": "object",
            "properties": {
                "file_path": {"type": "string"},
                "old_string": {"type": "string"},
                "new_string": {"type": "string"}
            },
            "required": ["file_path", "old_string", "new_string"]
        }
    },
    {
        "name": "run_command",
        "description": "Exécute une commande shell et renvoie stdout/stderr.",
        "input_schema": {
            "type": "object",
            "properties": {
                "command": {"type": "string"},
                "timeout": {"type": "integer"}
            },
            "required": ["command"]
        }
    },
    {
        "name": "search_code",
        "description": "Recherche un motif regex dans les fichiers d'un répertoire.",
        "input_schema": {
            "type": "object",
            "properties": {
                "pattern": {"type": "string"},
                "path": {"type": "string"},
                "file_glob": {"type": "string"}
            },
            "required": ["pattern"]
        }
    }
]
Enter fullscreen mode Exit fullscreen mode

Distribution du gestionnaire d'outils

Mappez chaque nom d'outil à sa fonction :

import subprocess
import os
import re

def execute_tool(name: str, params: dict) -> str:
    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"Erreur : Outil inconnu '{name}'"
    try:
        return handler(params)
    except Exception as e:
        return f"Erreur : {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"Écriture réussie dans {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"Erreur : '{old[:50]}...' introuvable dans {path}"
    if content.count(old) > 1:
        return f"Erreur : '{old[:50]}...' correspond à {content.count(old)} emplacements. Soyez plus spécifique."
    new_content = content.replace(old, params["new_string"], 1)
    with open(path, "w") as f:
        f.write(new_content)
    return f"Modification réussie de {path}"

def handle_run_command(params: dict) -> str:
    cmd = params["command"]
    timeout = params.get("timeout", 120)
    blocked = ["rm -rf /", "mkfs", "> /dev/"]
    for pattern in blocked:
        if pattern in cmd:
            return f"Erreur : Motif de commande dangereux bloqué : {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"Commande terminée avec le code de sortie {result.returncode}"
    if len(output) > 30000:
        output = output[:15000] + "\n\n... [tronqué] ...\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"Aucune correspondance trouvée pour le motif : {pattern}"
    lines = result.stdout.strip().split("\n")
    if len(lines) > 50:
        return "\n".join(lines[:50]) + f"\n\n... ({len(lines) - 50} correspondances supplémentaires)"
    return result.stdout
Enter fullscreen mode Exit fullscreen mode

Gestion du contexte : le point clé

Pourquoi la gestion du contexte est cruciale

La source montre que beaucoup d'efforts sont consacrés au compactage du contexte. À répliquer pour un agent DIY :

  • Auto-compactage : déclenché à ~92% de la fenêtre de contexte, avec tampon pour le résumé.
  • Réinjection de directives : Les fichiers de configuration projet sont réinjectés à chaque tour.

Compacteur basique

def maybe_compact(messages: list, system_prompt: str, max_tokens: int = 180000) -> list:
    """Compacter la conversation si trop longue."""
    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
    summary_response = client.messages.create(
        model=MODEL,
        max_tokens=4096,
        system="Résumez cette conversation. Gardez tous les chemins de fichiers, les décisions prises, les erreurs rencontrées et l'état actuel de la tâche. Soyez précis sur ce qui a été modifié et pourquoi.",
        messages=messages,
    )
    summary_text = summary_response.content[0].text
    compacted = [
        {"role": "user", "content": f"[Résumé de la conversation]\n{summary_text}"},
        {"role": "assistant", "content": "J'ai le contexte de notre conversation précédente. Sur quoi dois-je travailler ensuite ?"},
    ]
    compacted.extend(messages[-4:])
    return compacted
Enter fullscreen mode Exit fullscreen mode

Réinjection du contexte projet

def build_system_prompt(project_dir: str) -> str:
    """Construire une invite système avec le contexte du projet."""
    base_prompt = """Vous êtes un assistant de codage qui aide aux tâches d'ingénierie logicielle.
Vous avez accès à des outils pour lire, écrire, modifier des fichiers, exécuter des commandes et rechercher du code.
Lisez toujours les fichiers avant de les modifier. Préférez edit_file à write_file pour les fichiers existants.
Gardez les réponses concises. Concentrez-vous sur le code, pas sur les explications."""

    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# Directives du projet\n{project_context}"

    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# Directives du dépôt\n{root_context}"

    return base_prompt
Enter fullscreen mode Exit fullscreen mode

Le système de mémoire à trois couches

Couche 1 : MEMORY.md (toujours chargé)

Index léger (une ligne par entrée, <150 caractères), injecté dans l'invite système.

- [Préférences utilisateur](memory/user-prefs.md) - préfère TypeScript, utilise les raccourcis Vim
- [Conventions API](memory/api-conventions.md) - REST avec la spécification JSON:API, snake_case
- [Processus de déploiement](memory/deploy.md) - utilise GitHub Actions, déploie sur AWS EKS
Enter fullscreen mode Exit fullscreen mode

Couche 2 : fichiers thématiques (à la demande)

Fichiers de connaissances détaillés chargés uniquement si pertinents.

Couche 3 : transcriptions de session (accessibles par recherche)

Journaux complets jamais lus en bloc, consultés via grep.

Système de mémoire minimal

import json

MEMORY_DIR = ".agent/memory"

def load_memory_index() -> str:
    """Charge l'index de mémoire pour injection dans l'invite système."""
    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):
    """Enregistre une entrée mémoire et met à jour l'index."""
    os.makedirs(MEMORY_DIR, exist_ok=True)
    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}")
    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()
    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

Ajoutez un outil save_memory dans votre liste pour permettre à l'agent de sauvegarder ses connaissances.

Ajouter un système de permissions

Cinq modes dans Claude Code, mais pour un agent DIY, un système à trois niveaux suffit :

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:
    """Vérifie si l'utilisateur approuve l'appel d'outil."""
    risk = RISK_LEVELS.get(tool_name, "high")
    if risk == "low" and auto_approve_low:
        return True
    print(f"\n--- Vérification de permission ({risk.upper()} risque) ---")
    print(f"Outil : {tool_name}")
    for key, value in params.items():
        display = str(value)[:200]
        print(f"  {key}: {display}")
    response = input("Autoriser ? [o/n/toujours] : ").strip().lower()
    if response == "always":
        RISK_LEVELS[tool_name] = "low"
        return True
    return response == "y"
Enter fullscreen mode Exit fullscreen mode

Tester les appels API de votre agent avec Apidog

Construire un agent de codage implique de nombreux appels API à Claude. Pour analyser et rejouer ces requêtes efficacement, utilisez Apidog comme proxy et outil d'inspection.

Exemple Apidog

Capturer et rejouer les requêtes API

  1. Ouvrez Apidog et créez un projet pour votre agent.
  2. Importez POST https://api.anthropic.com/v1/messages.
  3. Configurez le corps de la requête avec votre prompt, outils et messages.
  4. Testez chaque tour individuellement en ajustant les paramètres.

Vous pouvez ainsi isoler des cas précis, ajuster les paramètres, et observer l'impact en temps réel.

Déboguer les conversations multi-tours

  • Enregistrez l'ensemble du tableau messages comme variable d'environnement après chaque tour.
  • Rejouez n'importe quelle étape de la conversation.
  • Comparez les résultats pour détecter où le comportement diverge.

Valider les schémas d'outils

Importez vos schémas d'outils dans Apidog et utilisez le validateur JSON pour détecter les erreurs avant l'envoi à l'API.

Tout assembler : le REPL complet

Voici un agent de codage minimal fonctionnel :

#!/usr/bin/env python3
"""Un agent de codage minimal de style Claude Code."""

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("Agent de codage prêt. Tapez 'quit' pour quitter.\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})
        messages = maybe_compact(messages, system_prompt)
        current_system = build_system_prompt(PROJECT_DIR)
        memory = load_memory_index()
        if memory:
            current_system += f"\n\n# Memory\n{memory}"
        result = agent_loop(current_system, TOOLS, messages)
        print(f"\n{result}\n")

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Cet agent lit, modifie, exécute des commandes, recherche dans le code, gère le contexte et conserve la mémoire entre les sessions, en moins de 300 lignes.

Ce qu'il faut ajouter ensuite

Plusieurs axes d'amélioration inspirés de la fuite :

Sous-agents pour le travail parallèle

Lancer des sous-agents avec agent_loop() sur des tâches indépendantes et fusionner les résultats.

Déduplication lecture de fichiers

Suivre les fichiers lus + mtime. Ne relire que si le fichier a changé.

Troncation/échantillonnage des sorties

Tronquer les résultats massifs d'outils (ex : grep) et indiquer au modèle le nombre d'occurrences omises.

Auto-compactage + réinjection de fichiers

Après compactage, réinjecter le contenu des fichiers récemment consultés (jusqu'à 5 000 jetons par fichier).

Ce que nous avons appris de la fuite

  • La boucle centrale est simple : 30 lignes suffisent, la complexité est dans les outils et la gestion du contexte.
  • Les outils dédiés surpassent bash : format structuré, sécurité, efficacité.
  • La mémoire doit être en couches : index permanent, fichiers thématiques à la demande, journaux accessibles par recherche.
  • La gestion du contexte est essentielle : auto-compactage, réinjection des directives, troncature des sorties.
  • Le harnais est le vrai produit : le modèle = intelligence, le harnais = perception + action + mémoire.

Pour tester et déboguer vos interactions API, y compris les conversations multi-tours et la validation des schémas, essayez Apidog gratuitement. Concentrez-vous sur la logique de l'agent, laissez Apidog gérer le debug API.

FAQ

Puis-je utiliser légalement les modèles de la fuite du code de Claude Code ?

La fuite révèle des modèles architecturaux mais pas d'algorithmes propriétaires. Recréez l'architecture avec votre propre code.

Quel modèle pour un agent de codage DIY ?

Claude Sonnet 4.6 : bon compromis vitesse/capacités. Opus 4.6 : meilleur pour l'architecture complexe, plus lent et coûteux. Haiku 4.5 : suffisant pour des tâches simples, 90 % moins cher.

Combien coûte l'exécution ?

30-50 tours avec Sonnet 4.6 coûtent 1 à 5 $. Compactez agressivement pour réduire les coûts.

Pourquoi React dans le terminal ?

Ink (React pour terminaux) facilite la gestion des vues complexes. Pour un agent DIY, un REPL suffit.

Fonctionnalité la plus importante après la boucle centrale ?

Le système de permissions : confirmation avant écriture/exécution.

Comment Claude Code gère-t-il les erreurs d'outils ?

Les erreurs sont renvoyées dans tool_result. Le modèle décide s'il faut réessayer ou demander à l'utilisateur.

Puis-je utiliser ce modèle avec d'autres LLM que Claude ?

Oui. Adaptez le format API. La boucle agent, les outils et la mémoire sont agnostiques au modèle.

Comment empêcher l'agent d'exécuter des commandes dangereuses ?

Liste noire (rm -rf /, mkfs...), confirmation explicite pour run_command, classement des risques comme dans Claude Code.


Pour aller plus loin et accélérer vos débogages API, essayez Apidog gratuitement.

Top comments (0)