J'ai arrêté de croire le "terminé" de l'agent - le gate verify.sh, prove-it
Un agent de codage IA annonce "tests passés" sans avoir lancé les tests. Ce n'est pas un mensonge, c'est un problème structurel. Ce que j'ai appris en construisant prove-it, un gate open source qui transforme le rapport en contrôle - et les quatre façons dont un hook maison de cinq lignes échoue en silence.
L'essentiel
Un agent de codage ne distingue pas ce qu'il a fait de ce qu'il voulait faire, alors il rapporte en réalité son intention. C'est pour ça qu'il dit "les tests passent" sans les avoir lancés. Aucun prompt ne corrige ça : il faut transformer le "terminé" en un contrôle à réussir plutôt qu'en déclaration. prove-it est un gate basé sur un hook Stop qui empêche l'agent de finir son tour tant que le verify.sh à la racine du dépôt ne retourne pas exit 0. Un hook maison de cinq lignes échoue en silence de quatre façons - il bloque une fois puis plus jamais, il laisse passer les tours qui se terminent par un commit, l'agent peut supprimer le contrôle lui-même, ou l'abandon devient indiscernable de la réussite. Ces quatre cas sont l'unique raison pour laquelle prove-it fait plus de cinq lignes.
Sur cette page
"Tous les tests passent. Vous pouvez merger."
L'agent m'a dit ça, et le diff était si propre que j'ai failli le croire sur parole. Puis j'ai remonté le terminal : les tests n'avaient pas tourné une seule fois. Aucun log d'exécution, nulle part. L'agent avait l'intention de lancer les tests, et cette intention s'est retrouvée telle quelle dans son rapport de fin.
Quand ça vous arrive plusieurs fois, la réaction est en général l'une des deux suivantes : ajouter au prompt "lance impérativement les tests et montre-moi les résultats", ou revérifier à la main chaque fois que l'agent dit avoir terminé. J'ai essayé les deux, aucune n'a tenu. Alors j'ai pris une troisième voie. Plutôt que de me méfier du mot "terminé", j'ai transformé le terminé en un contrôle à réussir, et non plus en déclaration. C'est comme ça qu'est né prove-it, un outil open source.
Ce n'est pas un mensonge, c'est structurel
Commençons par mettre les choses au clair : l'agent ne ment pas.
Un humain distingue "j'ai lancé les tests" de "j'avais l'intention de lancer les tests", parce qu'il a une mémoire. L'agent n'a pas ce point de comparaison. Faute de pouvoir confronter ce qu'il a fait à ce qu'il voulait faire, il rapporte l'intention au lieu du résultat. "Les tests passent" est en somme le raccourci de "j'ai écrit le code pour que les tests passent". Ce n'est pas un défaut de caractère du modèle, c'est une propriété de conception, et c'est pour ça qu'aucun prompt ne le corrige. Écrivez "lance-les absolument" cent fois : le jour où il ne les lance pas finira par arriver.
Les techniques de prompt ont une autre faiblesse : elles vieillissent à chaque génération de modèle. Qu'une consigne marche bien sur le modèle actuel ne garantit rien sur le suivant. Un dispositif qui exige des preuves, lui, est assis une couche au-dessus du modèle et survit aux mises à jour. Que les tests aient réellement tourné, ce n'est pas au modèle de le dire, c'est au code de sortie.
Transformer le rapport en contrôle
L'idée elle-même - n'accepter le "terminé" que comme une preuve, jamais comme une déclaration - je l'avais déjà traitée dans un article précédent. Cet article raconte comment cette idée est devenue un outil que chacun peut installer.
La convention de prove-it tient en une phrase. Le dépôt déclare comment se prouver lui-même dans un fichier nommé verify.sh, et l'agent ne peut pas revendiquer le terminé tant que cette preuve ne passe pas.
L'implémentation est la suivante. On place un verify.sh exécutable à la racine du dépôt. Exit 0 signifie "cet arbre est démontrablement sain". Quand l'agent tente de finir son tour, un hook Stop exécute ce script ; si le code de sortie n'est pas 0, au lieu de laisser l'agent s'arrêter, il le renvoie travailler avec les vingt dernières lignes de la sortie d'échec. Dans la plupart des cas, cette sortie suffit à l'agent pour corriger la cause.
Le déclenchement du gate est tout aussi simple. Cette session a réellement modifié ce dépôt, un verify.sh exécutable existe, et l'état actuel de l'arbre n'a pas encore passé le contrôle. Le gate ne tourne que si ces trois conditions sont vraies. Une session en lecture seule n'est jamais touchée, et un arbre déjà validé n'est pas recontrôlé.
Et le blocage n'est pas illimité. Trois renvois maximum par tour, puis le gate cède. Un hook qui ne cède jamais fige la session. Mais céder n'est pas passer : le dernier message d'un tour terminé par concession n'est pas "terminé", c'est l'avertissement "ce tour s'est achevé sans être vérifié". L'agent peut échouer à franchir le gate, mais il ne peut pas le franchir en silence. C'est tout ce que cet outil prétend réellement garantir.
Pourquoi cinq lignes de bash ne suffisent pas
Si arrivé ici vous vous dites "une ligne de commande de test dans un hook Stop et c'est réglé", vous avez raison. Cinq lignes suffisent, et c'est comme ça que j'ai commencé. Puis cette version a échoué en silence de quatre façons. Certaines étaient de vrais bugs des premières versions de prove-it, et chacune a désormais son test de régression.
Il bloque une fois, puis plus jamais. Après un premier blocage, Claude Code positionne le flag stop_hook_active sur tous les événements stop suivants. Un hook qui lit ce flag comme "laisse passer" cesse d'être un gate après exactement un blocage. À l'inverse, un hook qui ignore le flag bloque pour toujours et fige la session. C'est un piège qu'on ne comprend qu'après avoir subi les deux, et la réponse est de compter soi-même les tentatives, bloquer un nombre défini de fois, puis céder bruyamment.
Un commit, et tout a l'air de ne jamais avoir existé. Un hook qui juge "y a-t-il eu du travail" sur l'état sale du working tree laisse passer tous les tours qui se terminent par un commit. Or commiter est la chose la plus banale qu'un agent fasse. prove-it enregistre donc l'état de l'arbre au début de la session comme référence et le compare à chaque stop. Les commits, les fichiers modifiés au sed, les fichiers crachés par un générateur de code : tout est détecté comme changement.
L'agent peut supprimer le contrôle. Pour un agent qui n'arrive pas à faire passer verify.sh, le coup le moins cher est de supprimer verify.sh ou de lui retirer les droits d'exécution. Le gate enregistre au démarrage de la session si le dépôt était armé, et refuse tout tour qui se termine désarmé. Il existe un coup plus subtil : laisser le fichier exécutable et réécrire les contrôles à l'intérieur. Ça, le gate ne le bloque pas, parce que modifier verify.sh est souvent précisément la tâche demandée. Mais ça ne passe pas en silence non plus : si un tour valide alors que verify.sh a changé pendant la session, le gate le signale, et il suffit de lire ce diff pour juger si c'était du travail ou de l'esquive.
L'abandon est indiscernable de la réussite. Tout hôte finit par forcer le hook à céder. Un hook maison cède en silence, et le dernier mot que vous lisez est "terminé". Un tour qui a échoué trois fois puis abandonné et un tour validé du premier coup ont exactement la même tête à l'écran. Le dernier mot de prove-it est un avertissement. Si la différence vous paraît anecdotique, imaginez le lendemain du jour où vous avez mergé un tour abandonné en le croyant validé.
Ces quatre cas sont l'unique raison pour laquelle prove-it fait plus de cinq lignes. Si vous voulez garder votre propre hook, faites-le. Mais les cas qu'il doit couvrir sont écrits dans SPEC.md, et je vous conseille de les lire. Ce qui compte, ce n'est pas l'implémentation, c'est la convention.
Ce que cet outil ne prouve pas
Il y a une limite à consigner honnêtement. Le gate ne force qu'une seule chose : verify.sh a retourné 0 avant la fin du tour. Ce que ce 0 signifie dépend entièrement des contrôles que vous avez écrits. Un verify.sh réduit à exit 0 passe ce gate et ne prouve rien.
La spec appelle ça le Level 1. Le Level 2, c'est la question de savoir si les contrôles font des assertions sur des preuves réelles, et aucun outil ne peut vérifier ça à votre place. prove-it non plus. Brancher le hook est la partie facile. Le vrai travail, c'est répondre à la question "que signifie prouvé dans ce dépôt", et la réponse change d'un dépôt à l'autre. C'est pourquoi la convention ne fixe que le nom du fichier, jamais son contenu.
Les échappatoires sont ouvertes exprès. Avec PROVE_IT_SKIP=1, le tour passe sans contrôle, et supprimer verify.sh éteint le gate pour de bon. Un gate qu'on ne peut pas retirer, les gens finissent par le contourner, et un gate contourné est pire que pas de gate : il rapporte que les contrôles ont tourné alors que rien n'a tourné.
Le premier jour, volontairement petit
L'installation tient en trois lignes, et c'est la troisième qui fait le vrai travail.
/plugin marketplace add Why-Next/prove-it
/plugin install prove-it@whynext
/prove-it:init
/prove-it:init détecte la stack, crée verify.sh, vous montre sous vos yeux qu'il passe, puis exécute une copie à laquelle il a ajouté exit 1 pour vous montrer le gate refuser un tour. Comptez trente secondes.
Le verify.sh généré n'a qu'un seul contrôle actif : git diff --check. Les tests, le type check, tout le reste n'existe qu'en commentaires. C'est voulu, pour qu'il passe sur main le jour même de l'installation. Un gate qui échoue dès le premier jour apprend à l'équipe à le contourner dès la première semaine. Les contrôles commentés s'activent un par un, après les avoir lancés à la main et vus passer. Et avant d'en activer un, faites-le échouer exprès au moins une fois. Un contrôle qui ne peut pas échouer n'est pas un contrôle, et le découvrir le jour où vous en avez besoin, c'est trop tard.
Le point d'arrêt de la croissance de verify.sh est fixé aussi : une minute au total. Plus lent que ça, le contrôle part en CI. Les contrôles qui exigent des secrets ou un accès production sont aussi l'affaire de la CI. Dans verify.sh ne restent que les preuves que l'agent peut produire en local, pendant qu'il travaille.
Le registre de ce que le gate a attrapé
Avec PROVE_IT_LEDGER=1, chaque fois que le gate attrape un faux terminé, une ligne s'ajoute à un fichier local.
{"ts":"2026-07-09T04:12:33Z","claim":"All tests pass. Ready to merge.",
"evidence_demanded":"verify.sh exit 0","actual":["3 failed, 41 passed"]}
Ce que l'agent a affirmé, ce qui était exigé, ce qui s'est réellement passé. Après un mois d'accumulation, vous lisez la façon dont votre agent échoue dans un registre, au lieu de la deviner. Le fichier reste sur le disque local, n'est transmis nulle part, et est désactivé tant que vous ne l'activez pas.
Ce dépôt se gate lui-même
Le dépôt prove-it a évidemment son propre verify.sh, qui teste le gate lui-même sur de vrais dépôts git. Un contrôle qui échoue bloque-t-il bien ? Un contrôle qui passe ouvre-t-il bien ? Une session en lecture seule est-elle laissée tranquille ? Un arbre propre après commit n'est-il pas pris pour "aucun travail" ? Les quatre pièges décrits plus haut sont tous figés en tests de régression.
Licence MIT, et les seules dépendances sont bash, git et python3. Testé avec Claude Code ; le branchement pour les hôtes qui exposent le même genre de hook bloquant, comme Codex CLI ou Gemini CLI, est décrit dans la doc ADAPTERS. Si vous avez un dépôt où un agent modifie du code et dit "terminé" dans la même conversation, c'est exactement là que ce gate est le plus utile.
"Terminé" ne devrait pas être un mot que l'agent déclare, mais un état que le dépôt arbitre. Écrire les critères de cet arbitrage reste votre travail, mais au moins, plus personne ne peut désormais sauter l'arbitrage et passer en silence.
Questions fréquentes
Mon agent de codage IA annonce comme terminé un travail qu'il n'a pas fait. Comment l'en empêcher ?
Écrire dans le prompt "lance impérativement les tests" ne suffit pas. L'agent n'a aucun moyen de comparer ce qu'il a fait à ce qu'il voulait faire, alors il rapporte son intention telle quelle. Transformez plutôt la déclaration de fin elle-même en contrôle : placez un script de vérification (verify.sh) à la racine du dépôt, et quand l'agent tente de finir son tour, un hook Stop exécute ce script et le renvoie travailler si le code de sortie n'est pas 0. prove-it est l'implémentation de référence de cette convention.
Pourquoi ne pas écrire soi-même un script de cinq lignes qui lance les tests dans un hook Stop de Claude Code ?
C'est possible, mais il y a quatre pièges. ① Après le premier blocage, Claude Code positionne le flag stop_hook_active ; mal géré, il fait qu'on ne bloque qu'une fois ou qu'on bloque pour toujours, ce qui fige la session. ② Juger sur l'état sale du working tree laisse passer tous les tours qui se terminent par un commit. ③ L'agent peut désarmer le gate lui-même en supprimant verify.sh ou en lui retirant les droits d'exécution (chmod -x). ④ Quand le hook finit par céder, l'abandon ressemble à une réussite. Si vous l'écrivez vous-même, il faut couvrir ces quatre cas.
Quels contrôles mettre dans verify.sh ?
La réponse à la question "qu'est-ce qui doit être vrai pour qu'un changement soit prouvé dans ce dépôt". En général, on active un par un : la commande de tests réellement utilisée, le type check, puis le lint. Le tout doit tenir en moins d'une minute, et les contrôles qui exigent des secrets ou un accès production restent en CI. L'important est de ne pas viser trop haut dès le premier jour : un gate lent ou instable est contourné en moins d'une semaine, et un gate contourné est pire que pas de gate du tout.
Articles similaires
- 💻 Dev
Quand l'IA dit "c'est fait", le dispositif qui refuse de la croire
Les agents de codage IA disent que le travail est terminé alors qu'il ne l'est pas. Ils annoncent des tests réussis sans les avoir lancés, un bug corrigé sans l'avoir reproduit. L'histoire du garde-fou de vérification qu'un développeur solo, qui écrit presque tout son code avec l'IA, a mis en place pour n'accepter le "terminé" que sur preuves, jamais sur parole.
- 💻 Dev
La remise que vous ne touchiez pas : pourquoi Spot et Graviton coûtent si peu
La facture AWS a dépassé le budget deux mois de suite. En cherchant quelle ressource couper, j'ai compris que le vrai levier n'était pas « combien on consomme » mais « comment on achète ». Pourquoi Spot et Graviton sont si peu chers, et pourquoi certains coûts qui ressemblent à du gaspillage (RDS Proxy) ne doivent surtout pas être coupés - l'histoire du « pourquoi » derrière chaque ligne de la facture.
- 💻 Dev
Les hooks d'arrêt tournaient deux fois - quand SIGTERM a deux maîtres
À chaque déploiement, Sentry recevait une erreur Prisma P2028. En corrigeant l'ordre d'arrêt, j'ai découvert qu'enableShutdownHooks de NestJS et une bibliothèque de graceful shutdown captaient toutes les deux SIGTERM, faisant tourner les hooks d'arrêt deux fois à chaque fois.