AIDive

Claude Code ne dit « fini » qu'après les gates Shopify

Par AIDive · Publié le

Agents de codeAutomatisation & workflows

Intro : la gate qui refuse le « terminé »

Une gate, au sens où Shopify l'emploie, est une vérification qu'un changement écrit par une IA doit passer avant d'aller plus loin. Reconstruite dans Claude Code, seule la première a été rentable : les tests d'acceptation. Shopify a publié quatre gates et aucun chiffre, alors cette expérience les construit sur un vrai repository, lance cinquante requêtes en désactivant chaque gate à tour de rôle, et compte ce que chacune a attrapé et ce qu'elle a coûté.

Helix est devenu public le 21 septembre 2026. Il fait passer chaque checkpoint par quatre gates dans l'ordre : les tests, la revue de design, deux reviewers et un ingénieur. Le post ne donne aucun taux de réussite. Les reviewers sont la partie que la plupart des gens copient, et sur ces cinquante runs, le taux de réussite n'a bougé que lorsque les tests étaient dans la boucle. Les reviewers n'ont rien apporté, pour un coût environ trois fois plus élevé.

Deux autres constats sortent du même banc d'essai. Un hook censé être impossible à contourner a trois sorties dans sa propre documentation. Et quand les tests et les reviewers n'étaient pas d'accord, l'agent a fait quelque chose que personne n'avait prévu. Le décompte commence par le cas sans aucune gate.

Quatre gates, zéro chiffre, un banc d'essai

Le repository du banc d'essai est tinydb, une vraie base de données de documents, avec 226 tests au vert sur le commit utilisé.

Helix est l'outil interne de Shopify pour ramener ses applications mobiles vers du code natif. Son post énonce la règle sans détour : chaque checkpoint passe par quatre gates, dans l'ordre.

Gate Ce qu'elle fait dans le post de Shopify
Comportement Lance des tests où chaque cas pertinent doit passer
Design Demande à un second modèle de jouer le relecteur de design perfectionniste
Revue Deux agents reviewers indépendants, au contexte isolé ; la boucle recommence jusqu'à ce que les deux approuvent
Ingénieur Une personne boucle la boucle

La phrase qui vend le principe est « An attempt is allowed to be wrong. It is not allowed to ship until it isn't. » La gate de design tombe pour une bibliothèque, et le post dit qu'un changement purement logique la saute tout simplement.

Ce que le post ne donne jamais, c'est un chiffre. L'app Shop a été reconstruite en 12 semaines et l'app Shopify compte plus de 300 écrans, et c'est tout le registre. Il n'y a ni taux de réussite, ni coût, ni nombre de défauts échappés.

Le banc d'essai, ce sont cinq requêtes tapées comme un développeur les tape : des opérations push et pull, une requête between, le renommage d'une table, une recherche triée et une taille de cache en écriture. Derrière chaque requête se cachent cinq exigences que l'agent ne voit jamais, les choses qu'un senior rigoureux attendrait. Cela fait 25 exigences au total.

Le worker est Sonnet 5.5, lancé en headless. Cinq configurations fois cinq tâches fois deux répétitions donnent 50 runs. Une réserve s'applique : les versions explicites de ces requêtes passent du premier coup, donc les prompts ont été raccourcis à ce que les gens tapent vraiment.

Sans aucune gate, 35 des 50 exigences passent. Cela fait 70%, avec trois runs sur dix entièrement propres.

La tâche de tri montre à quoi ressemblent les ratés. L'agent a indexé chaque document avec le champ de tri, et dès qu'un document n'a pas ce champ, toute la recherche plante. Cinq exigences, zéro réussie, une clé manquante.

Le hook qui refuse le « terminé », et ses trois sorties

Un Stop hook est un script que Claude Code lance chaque fois que l'agent essaie de terminer son tour. Le guide d'Anthropic nomme lui-même le problème qu'il traite : Claude s'arrête quand le travail a l'air terminé. Sans vérification exécutable, « a l'air terminé » est le seul signal disponible, et c'est vous qui devenez la boucle de vérification.

Le hook vit dans le fichier de settings du projet, sous hooks, Stop, comme une seule commande avec un timeout. Ici, c'est un wrapper d'une ligne qui appelle le script de la gate. Le contrat, c'est le code de sortie : 0 laisse le tour se terminer, 2 le bloque, et tout ce que le script a écrit sur la sortie d'erreur standard revient à l'agent comme raison.

En pratique, ça se passe ainsi. L'agent résume son changement et s'arrête. Le hook lance les tests d'acceptation, l'un échoue, et le tour ne se termine pas. L'agent lit l'échec comme un retour du hook, ici un test qui attendait une erreur de type et ne l'a pas obtenue, corrige le code et s'arrête de nouveau. Cette fois la gate reste silencieuse.

Une règle dans le prompt est un conseil. Un hook n'en est pas un. Pourtant, la même page de documentation liste trois façons d'en sortir.

Sortie Ce que dit la documentation
Le plafond Après 8 poursuites de suite du tour par des stop hooks, Claude Code outrepasse le blocage suivant et met fin au tour. Le compteur se remet à zéro chaque fois que l'agent appelle un outil, et une variable d'environnement permet de le relever.
Le timeout 600 secondes par défaut pour un hook de type commande, et un hook qui dépasse son timeout ne rend aucune décision. Une suite de tests lente ouvre la gate toute seule.
Le flag L'entrée du hook porte stop_hook_active, à true quand la session continue déjà à cause d'un stop hook. La doc dit de le vérifier pour ne jamais bloquer sur une condition qui ne peut pas se résoudre.

La gate construite ici journalise ce flag et l'ignore. Elle abandonne d'elle-même après six blocages, pour que les boucles restent comptables.

Il existe une sortie de plus que personne ne documente. Après avoir été bloqué une fois, l'agent est parti chercher les tests : il a lancé cat sur le script du hook et cherché le fichier de test sur tout le disque. Seule la liste d'autorisations l'a arrêté. Les tests d'acceptation vivent donc hors de l'arborescence du repository, les settings interdisent la lecture du hook et des tests cachés, et l'agent voit l'échec sans jamais voir le fichier.

Gate 1 : les tests d'acceptation

La gate de tests d'acceptation est un ensemble de tests, écrits indépendamment de l'implémenteur, qui définissent ce que « terminé » veut dire. La première gate de Shopify n'est pas celle des tests de l'agent lui-même. Un subagent génère des cas de test de style intégration pour chaque checkpoint, en poussant vers des cas limites au-delà du chemin nominal, et ces tests définissent ce qui est prouvé.

Le rédacteur ici est donc un Sonnet headless distinct. Il voit la requête et le repository avant le changement, en lecture seule, ne peut pas exécuter de code, et reçoit pour consigne d'écrire un test par comportement qu'un senior rigoureux attendrait : champs manquants, mauvais types, bornes, persistance, le cache, et comportement existant inchangé. Il écrit entre 23 et 53 tests, 39 en moyenne, en environ 40 secondes. Le fichier atterrit hors du repository, donc quand un test échoue, l'agent lit l'assertion et jamais le test.

Les tests seuls font passer le taux de réussite de 70% à 84% : 42 exigences sur 50, et six runs entièrement propres au lieu de trois.

Tâche Sans tests Avec tests
Recherche triée 5 sur 10 10 sur 10
Taille du cache en écriture 8 sur 10 10 sur 10
Push et pull 4 sur 10 4 sur 10

Le plantage sur champ manquant de la tâche de tri est exactement le genre de cas que le rédacteur anticipe. Pour la tâche de cache, le rédacteur a exigé une erreur de valeur pour une taille de cache inférieure à un.

La tâche push et pull ne bouge pas. Elle rate trois exigences, dont deux concernent un document sans le champ, et la requête n'en disait rien. Le rédacteur se couvre exactement là où le prompt est muet : son test accepte une exception ou un no-op silencieux, donc la gate ne peut pas récupérer une exigence que personne n'a énoncée.

Sur les 30 runs gated avec tests, la gate a renvoyé l'agent 28 fois avec une vraie exigence en échec, et cinq fois alors qu'elles passaient toutes. L'une de ces cinq était la tâche de renommage : le rédacteur voulait que les noms qui ne sont pas des chaînes soient rejetés, personne ne l'avait demandé, et l'agent y a perdu un tour.

La première version de cette gate relançait les propres tests de l'implémenteur. Elle laissait passer chaque run dès son premier arrêt alors qu'une exigence cachée échouait, parce que les tests d'un agent testent ce qu'il a construit.

Le prix de la gate est de $0.20 par run au lieu de $0.085, et 60 secondes au lieu de 33.

Gate 3 : deux reviewers, comme l'écrit Shopify

La gate de revue est une paire d'agents reviewers, chacun travaillant dans un contexte neuf. Formulation de Shopify : deux agents reviewers indépendants, au contexte isolé, vérifient le nouveau code, chaque remarque doit être corrigée, et la boucle recommence jusqu'à ce que les deux reviewers approuvent. Le guide d'Anthropic explique pourquoi l'isolation compte : un reviewer dans un contexte neuf ne voit que le diff et les critères que vous lui donnez, pas le raisonnement qui a produit le changement.

Chaque reviewer ici est un Sonnet headless avec des outils en lecture seule et 15 tours au maximum. Il renvoie un verdict sous forme de JSON structuré : un flag approve et des remarques, chacune avec une sévérité blocking ou minor. Deux reviewers tournent en parallèle sur la requête et le diff complet, leurs remarques sont combinées, et la gate bloque dès que l'un des deux trouve quelque chose de blocking. Une paire coûte entre 3 et 6 cents par arrêt.

Configuration Exigences réussies Taux de réussite Blocages
Sans gate 35 sur 50 70% aucun
Reviewers seuls 34 sur 50 68% 1 run sur 10
Tests seuls 42 sur 50 84% 6 runs sur 10 (3 justes, 3 à tort)
Reviewers après les tests 41 sur 50 82% 2 runs sur 10, tous deux justes

Les reviewers seuls tombent deux points sous l'absence totale de gate, et les reviewers après les tests tombent deux points sous les tests seuls.

La tâche de tri explique le chiffre. Dans un run, les deux reviewers ont bloqué : le tri indexe le document, un document sans ce champ lève une erreur de clé, et toute la recherche échoue. L'agent a corrigé et réussi cinq sur cinq.

Dans deux autres runs, les deux reviewers ont vu exactement le même plantage et l'ont jugé minor. Un document sans le champ de tri lève une simple erreur de clé, et la docstring le documente. Les deux reviewers ont approuvé, et les deux runs ont fini à zéro sur cinq. Même modèle, même diff, même bug, deux sévérités différentes. Les reviewers n'ont jamais eu tort quand ils bloquaient. Le problème, c'est ce qu'ils laissent passer.

Un papier d'août a mesuré la même forme sur un benchmark public. Zero-shot 77%, un reviewer 77%, et deux reviewers indépendants 75%, le tout dans le bruit par rapport à l'absence de revue.

Configuration sur LiveCodeBench Score
Zero-shot 77%
Un reviewer 77%
Deux reviewers indépendants 75%
Reviewer plus critic (revue adversariale) 87%

Reviewer plus critic, et la boucle qui ne finit jamais

La revue adversariale associe un reviewer à un critic qui audite la revue par un désaccord structuré avant que l'agent principal ne modifie quoi que ce soit. Le même papier a trouvé que c'est la forme qui fonctionne. Elle a obtenu 87% là où deux reviewers obtenaient 75%, et elle a battu la meilleure base de référence à cinq agents, à 82%.

Benchmark Zero-shot Avec revue adversariale Coût
SWE-bench 71.6 75.2 environ 4.5x les tokens

Le critic est donc entré dans la gate. Le reviewer A relit. Le critic B reçoit les remarques plus le diff et tamponne chacune comme retenue ou rejetée. Seule une remarque blocking retenue ferme la gate.

Sur ce repository, le résultat a été de 39 sur 50, soit 78%. La paire critic n'a bloqué aucune fois en dix runs. Pendant la validation, elle a bien rejeté une fausse remarque sur laquelle la forme à deux reviewers aurait bloqué.

Ce que le critic n'a pas pu corriger, c'est la boucle. Trois runs ont atteint six blocages sur le même test d'acceptation, et la gate a abandonné alors que la vraie exigence échouait encore à chaque arrêt. Dans deux d'entre eux, le test bloquant était une erreur du rédacteur lui-même, et l'agent l'a dit : il ne modifierait pas le code pour un test qu'il ne pouvait pas lire.

Le troisième est celui à retenir. Le test d'acceptation voulait qu'une mise à jour qui échoue laisse tous les documents intacts, alors l'agent a réécrit la méthode de mise à jour de la table. Les reviewers ont bloqué cela, parce que le changement de la méthode de mise à jour n'était pas demandé et que la tâche consistait en deux nouvelles fonctions. L'agent a annulé. Son dernier message disait que le test d'acceptation et la revue de code demandaient des choses opposées, et qu'il s'était donc arrêté sans rien changer. Six blocages plus tard, le compteur l'a laissé sortir.

Le point d'abandon est ici fixé à six. Le plafond de Claude Code est de huit blocages consécutifs et se remet à zéro à chaque appel d'outil, donc il ne s'est jamais déclenché en cinquante runs.

La gate 4, c'est vous : ce qui arrive à l'humain, la facture, le kit

La quatrième gate, c'est l'ingénieur, et le post de Shopify promet que les gates ne deviennent pas plus laxistes quand personne ne regarde. Ce qui arrive à l'ingénieur, c'est tout ce que les autres gates ont laissé passer. Le registre ci-dessous répond à la question : quelle gate a rentabilisé son coût.

Configuration Réussies sur 50 Taux de réussite Coût par run Restant pour l'humain
Sans gate 35 70% $0.085 15
Tests d'acceptation 42 84% $0.20 8
Reviewers seuls 34 68% $0.15 16
Tests avec reviewers 41 82% $0.28 9
Tests avec reviewer plus critic 39 78% $0.28 11

Chaque ligne avec gate prend de 54 à 86 secondes par run, contre 33 sans gate.

Les blocages sur l'ensemble des cinquante runs se répartissent ainsi. Les tests ont renvoyé l'agent 28 fois à raison et cinq fois à tort. Les reviewers ont bloqué trois fois, jamais à tort, et trois boucles ont abandonné. Toute la matrice a coûté $9.95 de dépense API au tarif catalogue, telle que la CLI la rapporte, et environ 3,136 secondes, soit 52 minutes, de temps de worker cumulé.

Le verdict pour un repository comme celui-ci : gardez la gate de tests, avec un rédacteur qui n'est pas l'implémenteur et des tests que l'agent ne peut pas modifier. Attendez-vous à environ un faux blocage sur six, et donnez un compteur à la boucle. La gate à deux reviewers telle que Shopify l'a écrite n'a pas fait bouger le taux de réussite ici, sous aucune des deux formes. Si vous la gardez, gardez-la comme conseil dans le retour, pas comme barrière.

Un piège en chemin. La doc décrit un flag nu qui saute les hooks, les skills, les plugins et la mémoire pour les appels scriptés. Avec une connexion par abonnement, il ne lit qu'une clé d'API et renvoie une erreur, donc chaque reviewer est isolé à la place avec une source de settings vide et les hooks désactivés.

Les limites sont réelles : un repository, un modèle de worker, cinq tâches, deux répétitions par cellule. Un écart de deux points entre deux lignes, c'est du bruit. Les quatorze points entre l'absence de gate et la gate de tests n'en sont pas. La gate de design n'a pas été construite, et l'ingénieur n'a pas été chronométré.

Des quatre gates de Shopify, sur ce repository, seuls les tests ont été rentables. Tout le kit est dans le pack gratuit : le fichier de settings avec le hook et la deny list, le script stop, la gate, les commandes du rédacteur et des reviewers avec leurs schémas, et le modèle de registre.

Ressources gratuites

Sources

Questions fréquentes

Laquelle des quatre gates IA de Shopify vaut la peine d'être reconstruite dans Claude Code ?
La gate de tests d'acceptation. Sur 50 runs avec tinydb, elle a fait passer le taux de réussite de 70% à 84%, alors que la gate à deux reviewers ne l'a pas fait bouger (68% seule, 82% après les tests).
Comment empêcher Claude Code de dire qu'il a terminé avant que les tests passent ?
Ajoutez un Stop hook dans les settings du projet qui lance vos tests d'acceptation et sort avec le code 2 en cas d'échec. Claude Code bloque la fin du tour et renvoie à l'agent, comme raison, tout ce que le script a écrit sur la sortie d'erreur standard.
Peut-on vraiment forcer Claude Code à continuer de travailler avec un Stop hook ?
Pas complètement. La documentation des hooks liste un plafond de 8 blocages consécutifs qui se remet à zéro à chaque appel d'outil, un timeout par défaut de 600 secondes qui ne rend aucune décision, et le flag stop_hook_active qu'on vous dit de vérifier. Ajoutez votre propre compteur de blocages pour que les boucles se terminent de façon prévisible.
Deux reviewers de code IA indépendants améliorent-ils la sortie de l'agent ?
Pas dans ce test. Deux reviewers Sonnet ont obtenu 68% seuls et 82% après les tests, deux points sous chaque référence, parce qu'ils ont jugé minor un vrai plantage dans deux runs. Un papier a mesuré la même forme sur LiveCodeBench : 77% en zero-shot, 75% avec deux reviewers, 87% avec reviewer plus critic.
Combien coûte chaque gate par run dans Claude Code ?
Le coût par run au tarif catalogue de l'API était de $0.085 sans gate, $0.20 avec tests d'acceptation, $0.15 avec reviewers seuls et $0.28 avec tests plus reviewers. Les runs avec gate ont pris de 54 à 86 secondes contre 33.
Comment empêcher l'agent de lire les tests d'acceptation cachés ?
Stockez les tests hors de l'arborescence du repository et interdisez dans les settings la lecture du script du hook et des fichiers de test. L'agent voit alors l'assertion en échec, mais jamais le fichier.

Vidéos liées