Ça a commencé par une observation qui m'agaçait. Je mets dans mes fichiers d'instructions une règle simple — utiliser AskUserQuestion quand la compréhension n'est pas sûre — et régulièrement, elle est oubliée. Pas contournée volontairement : oubliée en cours de route, comme si la conversation avançait plus vite que la mémoire de la règle.

En creusant, j'ai vu que d'autres personnes qui travaillent avec des agents construisent des "hooks" — des scripts qui s'exécutent à des moments précis du fonctionnement de l'agent. Ma première question était un peu bête, mais utile : c'est encore une contrainte de plus à écrire quelque part, ou c'est autre chose que des instructions ?

C'est autre chose, et comprendre pourquoi m'a fait revoir une intuition que j'avais depuis un moment. Je pensais qu'un fichier d'instructions bien écrit, avec les bons champs de permissions, suffisait à garantir un comportement. Ce n'est pas vrai. Tout ce qui passe par le contexte d'un agent — un CLAUDE.md, une liste d'outils autorisés donnée à un sous-agent — reste, au fond, une suggestion. Rien ne garantit qu'elle sera honorée à un tour donné, et plus la conversation s'allonge, plus l'attention du modèle dérive des instructions du début.

Les hooks, eux, ne passent pas par le modèle. Ce sont des commandes shell exécutées hors de lui, à des points fixes de son cycle de vie : avant qu'il appelle un outil, après, quand il reçoit un message, quand il termine sa réponse. Ils reçoivent du JSON en entrée — quel outil, quels paramètres — et répondent par un code de sortie. Code 2 : ça bloque, même si le modèle "voulait" continuer. C'est la première fois que je vois une couche qui ne demande pas l'avis du modèle pour agir.

J'ai voulu vérifier si cette logique tenait aussi pour les sous-agents, parce que j'en construis pour séparer des tâches — un agent RH, un agent sécurité, un agent SEO sur ce site. Ma question : si je restreins la liste d'outils d'un sous-agent — pas d'Edit, seulement Read et Grep — est-ce que ça garantit qu'il ne peut pas écrire ? Réponse : non. Deux bugs ouverts et documentés sur le dépôt officiel de Claude Code montrent des sous-agents contournant les règles de permission alors même que l'outil concerné n'était pas dans leur liste autorisée. Et même sans bug : un sous-agent qui a accès à Bash peut modifier un fichier avec un simple sed -i, sans jamais appeler l'outil "Edit" nommé comme tel. La restriction sur les outils nommés ne couvre pas ce qu'on peut faire à côté.

Ça m'a forcé à revoir ma façon de penser la sécurité de mes propres architectures d'agents. La vraie garantie, ce n'est pas un champ d'outils autorisés bien rempli, c'est un hook qui s'exécute avant le système de permission et qui peut bloquer un appel, quel que soit l'agent qui l'a fait, quel que soit le chemin pris pour y arriver. Une image qui m'a parlé, lue dans une discussion sur le sujet : une porte sans poignée, plutôt qu'une porte avec une pancarte "ne pas ouvrir".

Trois chemins de contournement d'une liste d'outils restreinte, tous bloqués par un même hook Un sous-agent limité à Read et Grep atteint quand même le fichier par trois chemins différents (deux bugs documentés et une commande shell détournée) ; seul un hook PreToolUse, placé avant le système de permission, intercepte les trois à la fois. Sous-agent tools: Read, Grep — pas Edit Bug #25000 contourne le refus de permission Bug SDK #115 appelle Edit malgré la liste sed -i via Bash jamais nommé "Edit" Hook PreToolUse — avant le système de permission Protégé, quel que soit le chemin (sans hook : fichier modifié quand même)
Un champ tools: restreint (ici Read/Grep, sans Edit) reste une restriction déclarée, pas une garantie : deux bugs documentés sur le dépôt officiel (issues #25000 et SDK #115, voir Références) et un simple détour par Bash suffisent à l'atteindre quand même. Seule la barrière du bas — un hook PreToolUse, hors du modèle — intercepte les trois chemins en même temps, quel que soit celui emprunté.

Ce que je n'avais pas vu non plus : un hook n'a aucun accès à l'état interne du modèle. Il ne voit que des événements observables de l'extérieur. Ma règle de départ — demander si l'agent n'est pas sûr — n'est pas hookable telle quelle, parce que "le modèle n'était pas sûr" n'est pas une donnée qu'un hook peut lire. Ce n'est pas que je m'y prends mal, c'est hors de portée par construction. Ce qui devient hookable, c'est la version reformulée en condition observable : pas "demande si incertain", mais "bloque toute écriture tant qu'aucun appel à AskUserQuestion n'a eu lieu dans cette tâche". Présence ou absence d'un événement, ça, un hook sait le vérifier.

Autre angle mort découvert en creusant : je pensais qu'un agent principal "reprenait la main" si la réponse d'un sous-agent ne lui convenait pas. Faux, par défaut. Un sous-agent tourne dans son propre contexte isolé et ne retourne qu'une chaîne de résultat final au parent. Rien ne vérifie automatiquement que ce résultat est bon. Si je veux qu'une mauvaise réponse soit rattrapée, il faut construire l'étape de validation moi-même — ce n'est pas un réflexe intégré à l'outil, c'est un choix d'architecture à faire.

Ça éclaire aussi un truc qui m'agaçait sans que j'en comprenne la cause : chez le client, avec Copilot, même en sélectionnant Sonnet comme modèle, ça part parfois n'importe comment. Copilot n'a pas d'équivalent officiel de premier ordre pour les hooks — je n'ai pas trouvé de trace d'une couche déterministe comparable dans ses réglages. Ça veut dire que la seule main que j'y ai, c'est la couche instructions, probabiliste. Le modèle est le même que celui que j'utilise ailleurs, mais le harnais autour n'offre pas la même prise. Ça confirme une distinction que j'avais un peu ratée : le modèle et le harnais sont deux choses séparées, et une bonne partie de la fiabilité perçue d'un outil se joue dans le harnais, pas dans le modèle branché dessus.

Où se joue le harnais, couche par couche
Couche Claude Code Copilot
Instructions générales CLAUDE.md (racine + global) copilot-instructions.md ; AGENTS.md aussi lu
Instructions par chemin CLAUDE.md imbriqués fichiers *.instructions.md + applyTo
Permissions / comportement settings.json (projet, perso, global) pas d'équivalent officiel de même maturité
Hooks (déterministe) clé hooks dans settings.json rien d'officiel de premier ordre trouvé
Agents spécialisés fichiers d'agents dédiés pas de format perso officiel ; Copilot Studio côté entreprise
Skills SKILL.md par dossier "prompt files", moins standardisé
MCP fichier de config projet ou perso supporté aussi
Le fossé avec Copilot ne tient pas qu'aux hooks (ligne surlignée) : sur la moitié des couches qui composent le harnais, Claude Code expose un réglage officiel là où je n'ai rien trouvé d'équivalent côté Copilot — d'où la seule main qui me reste, chez le client, est la couche instructions, probabiliste.

Je ne tire pas de tout ça une promesse de contrôle total. Un hook n'a de prise que sur ce qui est observable et formulable en condition binaire — tout ce qui reste un jugement pur, "est-ce que c'était vraiment ambigu", aucun hook ne le fait à ma place. Ce n'est pas un trou dans les hooks, c'est la frontière naturelle entre ce qu'une couche déterministe peut garantir et ce qui reste mon rôle de relecture. Mais je sais maintenant où est cette frontière — et je ne demande plus poliment à un prompt de faire le travail d'une porte fermée à clé.

Références