L'architecture des outils de Claude Code

@spandan_madan
ANGLAISil y a 4 semaines · 17 juin 2026
552K
416
115
86
479

TL;DR

Cette analyse technique approfondie explore l'architecture des outils de Claude Code, détaillant son pipeline de répartition en sept phases, son planificateur de concurrence et son système de permissions « fail-closed » qui permet des actions complexes d'agents IA.

Une analyse approfondie de la manière dont Claude Code découvre, distribue et exécute les outils

Introduction

Dans un précédent article, nous avons rétro-ingéniéré le système de mémoire d'Anthropic Harness à partir de son code source divulgué. La mémoire s'est avérée étonnamment simple — des fichiers Markdown, du frontmatter et de l'ingénierie de prompts. Le système d'outils est tout le contraire. C'est le sous-système le plus élaboré de toute la base de code : 43+ outils, un pipeline d'exécution en streaming, un système d'autorisations à plusieurs niveaux, un framework de hooks et un ordonnanceur de concurrence — le tout relié pour transformer un modèle de langage sans état en quelque chose capable de lire des fichiers, d'exécuter des commandes shell, de rechercher sur le Web et de générer des sous-agents.

Cet article passe en revue le cycle de vie d'un outil, depuis sa définition jusqu'à la façon dont les appels d'outils du modèle sont distribués, et comment les résultats reviennent dans la conversation. De manière générale, le système se compose de quatre couches : une interface d'outil que chaque outil implémente, un registre qui assemble le pool d'outils, un pipeline de distribution qui valide, vérifie les autorisations et exécute chaque appel, et un ordonnanceur de concurrence qui décide ce qui s'exécute en parallèle.

L'architecture en un coup d'œil

Spandan Madan - inline image

L'interface d'outil

Chaque outil dans Claude Code implémente la même interface, définie dans Tool.ts. Le type est générique sur trois paramètres : Input (un schéma Zod), Output (le type de résultat) et P (données de progression). En pratique, un outil est un objet avec environ 30 méthodes, mais seules quelques-unes comptent pour comprendre le système.

La structure de base :

text
1type Tool<Input, Output, P> = {
2 name: string
3 inputSchema: ZodType // Schéma Zod pour la validation des entrées
4 call(input, context, canUseTool,
5 parentMessage, onProgress): Promise<ToolResult>
6
7 // Déclarations de comportement
8 isConcurrencySafe(input): boolean // Peut s'exécuter en parallèle ?
9 isReadOnly(input): boolean // Opération en lecture seule ?
10 isDestructive(input): boolean // Action destructive ?
11
12 // Autorisation et validation
13 checkPermissions(input, context): Promise<PermissionResult>
14 validateInput(input, context): Promise<ValidationResult>
15
16 // Intégration API
17 description(input, options): Promise<string>
18 prompt(options): Promise<string> // Texte du *prompt* système pour cet outil
19 mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam
20
21 // Rendu UI (React)
22 renderToolUseMessage(input, options): ReactNode
23 renderToolResultMessage(content, ...): ReactNode
24}

Aucun outil n'implémente cela à partir de zéro. Une fonction d'usine appelée buildTool() remplit des valeurs par défaut sûres :

Spandan Madan - inline image

Les valeurs par défaut sont délibérément conservatrices. Un auteur d'outil qui oublie de déclarer la sécurité de concurrence obtient une exécution séquentielle. Un auteur d'outil qui oublie d'implémenter les vérifications d'autorisation obtient le flux d'autorisation par défaut. Le système échoue en mode fermé.

Le type ToolResult mérite d'être noté :

text
1type ToolResult<T> = {
2 data: T // La sortie réelle
3 newMessages?: Message[] // Messages de suivi optionnels
4 contextModifier?: (ctx) => ToolUseContext // Modifier le contexte pour l'outil suivant
5 mcpMeta?: { ... } // Métadonnées du protocole MCP
6}

Le contextModifier est important — il permet à un outil de modifier le contexte d'exécution pour les outils suivants dans le même tour. C'est ainsi que des outils comme EnterWorktree changent le répertoire de travail pour tout ce qui suit. De manière cruciale, les modificateurs de contexte ne sont autorisés que pour les outils non concurrentiels. Si un outil s'exécute en parallèle, il ne peut pas modifier l'état partagé.

Le registre d'outils

Tous les outils sont enregistrés dans une seule fonction : getAllBaseTools() dans tools.ts. Elle retourne un tableau plat. Certains outils sont toujours présents ; d'autres sont conditionnés par des feature flags, des variables d'environnement ou des vérifications de plateforme.

Toujours disponibles (16 outils)

Spandan Madan - inline image

Outils conditionnés par des feature flags (~27 outils)

Les outils restants sont inclus conditionnellement. Certains sont conditionnés par des variables d'environnement (USER_TYPE=ant pour les outils internes d'Anthropic comme config et tungsten). Certains sont conditionnés par des feature flags via Statsig (web_browser, sleep, monitor). Certains sont spécifiques à une plateforme (powershell sur Windows). Certains sont conditionnés par des conditions composées — l'outil repl nécessite à la fois USER_TYPE=ant et un feature flag REPL.

Liste complète des outils conditionnés par des feature flags

Ant uniquement : config, tungsten, suggest_background_pr, repl (nécessite également le flag REPL)

Feature flags : web_browser, web_search, sleep, monitor, overflow_test, ctx_inspect, terminal_capture, list_peers, workflow, snip

Déclencheurs d'agents : cron_create, cron_delete, cron_list, remote_trigger

Kairos (agent proactif) : sleep, send_user_file, push_notification, subscribe_pr

Essaims multi-agents : team_create, team_delete, send_message

Todo v2 : task_create, task_get, task_update, task_list

Environnement : lsp (ENABLE_LSP_TOOL), enter_worktree / exit_worktree (mode worktree), powershell (Windows)

Découverte d'outils : tool_search (lorsque le pool d'outils est grand)

Test uniquement : testing_permission (NODE_ENV=test)

Outils MCP

Au-delà des outils intégrés, Claude Code prend en charge les serveurs Model Context Protocol (MCP) — des processus externes qui exposent leurs propres outils via un protocole standardisé. Les outils MCP sont enregistrés dynamiquement à l'exécution à partir des serveurs connectés et enveloppés dans la même interface Tool. Du point de vue du pipeline de distribution, un outil MCP est indistinguable d'un outil intégré.

Chaque outil MCP porte des métadonnées sur son serveur d'origine (mcpInfo: { serverName, toolName }), utilisées pour les règles d'autorisation, la gestion des erreurs et l'authentification. Lorsqu'un outil MCP échoue avec une erreur d'authentification, le système met automatiquement à jour le statut du serveur en needs-auth et signale le problème à l'utilisateur.

Assemblage du pool d'outils

Trois fonctions assemblent l'ensemble final d'outils :

  1. getAllBaseTools() — retourne la liste brute de 43+ outils intégrés avec les feature flags appliqués
  2. getTools(permissionContext) — filtre selon les règles de refus et isEnabled()
  3. assembleToolPool(permissionContext, mcpTools) — fusionne les outils intégrés et MCP

La stratégie de fusion dans assembleToolPool() est délibérée :

Les outils intégrés viennent en premier, donc en cas de collision de noms, l'outil intégré l'emporte. Le tri alphabétique au sein de chaque partition maintient l'ordre stable entre les sessions, ce qui est important pour la mise en cache des prompts — le tableau d'outils fait partie de la requête API, et le réordonner viderait le cache.

La stratégie de fusion dans assembleToolPool() est délibérée :

text
1// Trier chaque partition par ordre alphabétique, concaténer, dédupliquer
2const byName = (a, b) => a.name.localeCompare(b.name)
3return uniqBy(
4 [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
5 'name',
6)

Sérialisation API

Avant que les outils n'atteignent l'API Claude, toolToAPISchema() convertit le schéma Zod de chaque outil au format JSON Schema de l'API Anthropic.

Le pipeline de distribution

Lorsque Claude répond, son message peut contenir des blocs tool_use — des requêtes structurées pour invoquer des outils. Le pipeline de distribution traite ces blocs en sept phases. Chaque appel d'outil passe par toutes les phases, dans l'ordre.

Phase 1 : Extraction

Dans la boucle de requête principale (query.ts), les blocs tool_use sont filtrés du message de l'assistant :

text
1const msgToolUseBlocks = message.message.content.filter(
2 content => content.type === 'tool_use',
3) as ToolUseBlock[]

Chaque bloc a un name, un objet input, et un id unique. L'id est crucial — le résultat de l'outil doit référencer le même id lorsqu'il est renvoyé à l'API, sinon la conversation est rompue.

Phase 2 : Validation des entrées

Le schéma Zod de l'outil valide l'entrée brute en utilisant safeParse() — une variante non génératrice d'erreur qui retourne soit des données valides, soit une erreur structurée. Si la validation échoue, le modèle reçoit un message d'erreur formaté avec des indications sur le schéma, et l'exécution s'arrête pour cet outil. Aucun code ne s'exécute sur une entrée invalide.

text
1const parsedInput = tool.inputSchema.safeParse(input)
2if (!parsedInput.success) {
3 let errorContent = formatZodValidationError(tool.name, parsedInput.error)
4 // Retourner l'erreur au modèle, ignorer l'exécution
5}

Après la validation Zod, certains outils exécutent une deuxième vérification validateInput() pour une validation sémantique qui ne peut pas être exprimée dans un schéma — par exemple, vérifier qu'un chemin de fichier est absolu, pas relatif.

Phase 3 : Hooks pré-outil

Avant les vérifications d'autorisation, les hooks configurés par l'utilisateur s'exécutent. Ce sont des commandes shell externes ou des scripts qui se déclenchent lors des invocations d'outils. Un hook pré-outil peut :

  • Autoriser l'appel d'outil, en contournant l'invite d'autorisation interactive
  • Refuser l'appel d'outil carrément
  • Modifier l'entrée avant l'exécution
  • Bloquer l'exécution avec un message d'erreur
  • Fournir un contexte supplémentaire à l'utilisateur

Un invariant critique : l'autorisation d'un hook ne contourne pas les règles de refus des paramètres. Le code source contient un commentaire explicite à ce sujet : "L''allow' d'un hook ne contourne PAS les règles deny/ask de settings.json." L'intention est que les hooks peuvent ouvrir des portes, mais pas outrepasser les verrous.

Phase 4 : Vérification des autorisations

Le système d'autorisation est la partie la plus complexe du pipeline. Il se résout à travers plusieurs couches, dans l'ordre :

  1. Règles de refus — vérifiées en premier. Si une règle de refus correspond, l'exécution s'arrête immédiatement. Les règles de refus sont définitives et ne peuvent être annulées par aucune autre couche.
  2. Règles de demande — si elles correspondent, l'utilisateur est invité à approuver (sauf autorisation automatique en mode sandbox pour Bash).
  3. Autorisations spécifiques à l'outil — la propre méthode checkPermissions() de l'outil s'exécute. BashTool, par exemple, analyse la commande pour vérifier les règles au niveau des sous-commandes.
  4. Vérifications de sécurité — protections codées en dur pour les chemins sensibles (.git/, .claude/, configurations shell). Celles-ci sont insensibles au contournement — même en mode bypass complet, elles nécessitent une approbation interactive.
  5. Mode d'autorisation — le mode configuré par l'utilisateur détermine le comportement par défaut.
  6. Règles d'autorisation — vérifiées en dernier. Si une règle d'autorisation correspond et qu'aucune règle de refus/demande n'a été déclenchée, l'outil procède.

Modes d'autorisation

default — Toujours demander à l'utilisateur pour les décisions "ask".

acceptEdits — Autoriser automatiquement les opérations sécurisées sur les fichiers (lecture, édition), demander pour tout le reste.

bypassPermissions — Autoriser automatiquement tout sauf les règles de refus et les vérifications de sécurité.

plan — Approuver d'abord un plan, puis suivre le mode précédent pour l'exécution.

auto — Utiliser un classifieur IA pour décider d'autoriser ou de demander.

dontAsk — Convertir toutes les décisions "ask" en "deny" — ne jamais demander, simplement refuser.

Les règles d'autorisation proviennent de plusieurs sources, résolues par ordre de priorité : policySettings, localSettings, projectSettings, userSettings, flagSettings, cliArg, command, session. Cela permet aux politiques de l'organisation de remplacer les préférences utilisateur, et aux arguments CLI de remplacer les deux.

Phase 5 : Exécution

Si l'autorisation est accordée, la méthode call() de l'outil est invoquée :

text
1const result = await tool.call(
2 callInput,
3 { ...toolUseContext, toolUseId: toolUseID },
4 canUseTool,
5 assistantMessage,
6 progress => onToolProgress({ toolUseID: progress.toolUseID, data: progress.data })
7)

Cinq arguments : l'entrée validée, un contexte d'exécution (répertoire de travail, contrôleur d'abandon, état de l'application), un callback d'autorisation (pour les outils qui doivent demander des autorisations supplémentaires en cours d'exécution), le message assistant parent, et un callback de progression pour les mises à jour en temps réel. La durée est suivie globalement.

Un détail subtil : l'entrée passée à call() est l'entrée originale du modèle, pas la version complétée que les hooks et les autorisations ont vue. Cela préserve la cohérence de la transcription — l'appel d'outil enregistré dans la conversation correspond exactement à ce que le modèle a généré.

Phase 6 : Hooks post-outil

Après l'exécution, les hooks post-outil se déclenchent. Ceux-ci peuvent modifier la sortie de l'outil MCP, fournir un contexte supplémentaire, ou bloquer la poursuite de la conversation. Il existe également un hook PostToolUseFailure distinct qui ne se déclenche qu'en cas d'erreur, donnant aux systèmes externes la possibilité de journaliser les échecs ou de suggérer des mesures correctives.

Phase 7 : Mappage des résultats

Chaque outil implémente mapToolResultToToolResultBlockParam() pour convertir sa sortie au format ToolResultBlockParam de l'API Anthropic — un bloc tool_result avec une référence tool_use_id et un contenu textuel ou structuré.

Si le résultat dépasse un seuil de taille, il est persistant sur le disque à sessionDir/tool-results/{toolUseId}.txt et un aperçu avec une référence de fichier est envoyé à l'API à la place. Cela empêche les sorties volumineuses (une lecture de fichier de 10 000 lignes, une sortie de commande verbeuse) de gonfler le contexte de la conversation.

L'ordonnanceur de concurrence

Lorsque le modèle émet plusieurs appels d'outils dans un seul message, ils ne s'exécutent pas tous en même temps. Un ordonnanceur les partitionne en lots en fonction de la sécurité de concurrence.

L'algorithme est simple. Parcourir les appels d'outils dans l'ordre. Pour chacun, vérifier isConcurrencySafe(input). S'il est sûr et que le lot précédent était également sûr, l'ajouter au lot. Sinon, démarrer un nouveau lot.

text
1// Simplifié à partir de toolOrchestration.ts
2for (const toolUse of toolUseMessages) {
3 const isSafe = tool.isConcurrencySafe(parsedInput)
4 if (isSafe && lastBatch.isConcurrencySafe) {
5 lastBatch.blocks.push(toolUse) // Fusionner dans le lot concurrent
6 } else {
7 batches.push({ isConcurrencySafe: isSafe, blocks: [toolUse] })
8 }
9}

Les lots sûrs s'exécutent en concurrence (jusqu'à une limite de 10, configurable via CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY). Les lots non sûrs s'exécutent séquentiellement, un outil à la fois. Les modificateurs de contexte ne sont appliqués qu'entre les lots, pas à l'intérieur de ceux-ci.

En pratique, cela signifie qu'un message comme "lis ces 5 fichiers" produit un lot concurrent, tandis que "lis ce fichier, puis modifie-le" produit deux lots séquentiels. Le modèle peut même déclencher les deux motifs en un seul tour — les appels en lecture seule consécutifs sont regroupés, et la première écriture brise le lot.

L'exécuteur en streaming

Il existe un deuxième chemin d'exécution : le StreamingToolExecutor. Lorsque le streaming est activé, les outils commencent à s'exécuter pendant que le modèle génère encore sa réponse. Dès qu'un bloc tool_use se termine dans le flux, il est immédiatement mis en file d'attente pour exécution plutôt que d'attendre la réponse complète.

L'exécuteur en streaming utilise les mêmes règles de concurrence mais ajoute un comportement : la cascade d'erreurs Bash. Si une commande Bash échoue alors que des outils frères s'exécutent en parallèle, l'exécuteur abandonne tous les frères. La logique est qu'une commande Bash en échec invalide probablement le contexte sur lequel les autres outils opèrent — continuer avec eux fait perdre du temps et peut provoquer des erreurs confuses.

text
1if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {
2 this.hasErrored = true
3 this.siblingAbortController.abort('sibling_error')
4}

Un exemple concret

Pour rendre cela concret, traçons ce qui se passe lorsque le modèle décide de lire un fichier. Le modèle émet :

text
1{
2 "type": "tool_use",
3 "id": "toolu_01XYZ",
4 "name": "read",
5 "input": { "file_path": "/src/index.ts" }
6}

Extraction : query.ts filtre cela du contenu du message de l'assistant.

  1. Recherche d'outil : findToolByName(tools, "read") trouve FileReadTool.
  2. Validation des entrées : Zod analyse { file_path: "/src/index.ts" } par rapport à z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) . Cela passe.
  3. Hooks pré-outil : Tous les hooks configurés par l'utilisateur se déclenchent. Aucun ne modifie l'entrée.
  4. Vérification des autorisations : La méthode checkPermissions() de FileReadTool appelle checkReadPermissionForTool() . Les outils de lecture sont généralement autorisés dans la plupart des modes d'autorisation.
  5. Exécution : FileReadTool.call() lit le fichier, applique la numérotation des lignes (format cat -n), et traite les PDF/images/notebooks comme des cas spéciaux.
  6. Mappage des résultats : Le contenu du fichier devient un bloc tool_result référençant "toolu_01XYZ".
  7. Retour : Le résultat est ajouté à la conversation en tant que message utilisateur et envoyé dans l'appel API suivant.

Étant donné que FileReadTool déclare isConcurrencySafe: () => true et isReadOnly: () => true, si le modèle avait émis cinq appels de lecture dans le même message, les cinq s'exécuteraient en parallèle.

Résumé

Le système d'outils est l'épine dorsale d'exécution de Claude Code. Il prend l'intention du modèle — exprimée sous forme de blocs structurés tool_use — et la transforme en actions réelles sur votre machine, avec validation, autorisations et contrôle de concurrence à chaque étape.

La conception est en couches : une usine buildTool() conservative assure des valeurs par défaut sûres, un registre conditionné par des feature flags contrôle ce qui est disponible, un pipeline de distribution en sept phases valide et vérifie les autorisations de chaque appel, et un ordonnanceur de concurrence maximise le parallélisme tout en préservant la correction. L'exécuteur en streaming ajoute une optimisation de performance par-dessus — les outils commencent à s'exécuter avant que le modèle n'ait fini de réfléchir.

Comparé au système de mémoire (5 chemins, un répertoire de fichiers Markdown et de l'ingénierie de prompts), le système d'outils est un véritable environnement d'exécution. C'est la différence entre un classeur et un système d'exploitation.

Ce qui est intéressant

Le modèle en tant qu'ordonnanceur

L'ordonnanceur de concurrence est réactif — il regroupe ce que le modèle émet. Mais le modèle lui-même est le véritable ordonnanceur. Le prompt système lui dit de "faire tous les appels d'outils indépendants en parallèle" et "d'utiliser un seul appel Bash avec && pour enchaîner les commandes dépendantes". L'exécution lui fait confiance. Si le modèle émet cinq lectures suivies d'une écriture, l'ordonnanceur parallélisera les lectures et séquentialisera l'écriture. Mais c'est le modèle qui a décidé de cet ordre. L'ordonnanceur exécute le plan du modèle, pas le sien propre.

Échec en mode fermé par défaut

Le principe de conception le plus cohérent : tout échoue en mode fermé. Outil inconnu ? Erreur. Entrée invalide ? Erreur. Pas de déclaration de concurrence ? Exécution séquentielle. Pas de déclaration d'autorisation ? Demander à l'utilisateur. Pas de feature flag ? L'outil n'existe pas. C'est inhabituel pour un système dont l'utilisateur principal est un modèle d'IA qui pourrait halluciner des noms d'outils ou mal former les entrées. Le système est conçu pour contenir les erreurs du modèle, pas pour les accommoder.

Les hooks comme point d'extension

Le système de hooks — pré-outil, post-outil et post-échec — est le principal point d'extension. C'est ainsi que les organisations appliquent des politiques (règles de refus dans les hooks pré), que les systèmes de journalisation capturent l'utilisation des outils (hooks post), et que les pipelines CI/CD s'intègrent (hooks d'échec). Important : les hooks ne peuvent que resserrer les restrictions, pas les assouplir. Un hook peut refuser un outil que les paramètres autorisent, mais il ne peut pas autoriser un outil que les paramètres refusent.

43 outils, 1 interface

Peut-être la chose la plus frappante est l'uniformité. Une commande bash, un web_fetch, un sous-agent, une création de tâche cron et une notification push implémentent tous la même interface de 30 méthodes, passent par le même pipeline en sept phases et respectent le même système d'autorisations. Il n'y a pas de cas spéciaux dans le répartiteur. La complexité réside dans les implémentations individuelles des outils et dans les règles d'autorisation, pas dans le routage.

Enregistrer en un clic

Lire les articles viraux en profondeur avec l’IA de YouMind

Enregistrez la source, posez des questions ciblées, résumez l’argument et transformez un article viral en notes réutilisables dans un seul espace de travail IA.

Découvrir YouMind
Pour les créateurs

Transformez votre Markdown en un article 𝕏 impeccable

Quand vous publiez vos propres textes longs, la mise en forme 𝕏 des images, tableaux et blocs de code est pénible. YouMind transforme un brouillon Markdown complet en un article 𝕏 impeccable, prêt à publier.

Essayer Markdown vers 𝕏

D'autres patterns à décoder

Articles viraux récents

Explorer plus d'articles viraux