Le bug que Claude vous a fait coder vendredi dernier (sans que vous le sachiez)
Un vendredi de février 2026, je relisais une PR que Claude venait d'écrire pour le module de réservation de créneau (slot-hold) de crmcoaching. 47 lignes, tests verts, tout avait l'air propre. Sauf qu'au beau milieu du use case holdSlot se cachait une race condition qui, en prod, aurait permis à deux clients de réserver le même créneau au même instant. Personne ne l'avait vue. Pas même moi, au premier regard.
C'est ce genre de PR qui m'a fait changer ma méthode de review depuis que je développe crmcoaching avec Claude. Voici ce que j'ai appris à repérer, pourquoi Claude écrit ce genre de code, et la grille en 4 points que j'applique aujourd'hui sur chaque PR IA-générée.

Pourquoi les bugs Claude passent en review
Le constat est simple, et il est documenté. Selon le GitClear AI Copilot Code Quality Report 2024, les équipes qui s'appuient sur Claude voient le ratio de code "moved or refactored" baisser de 39%, et le code "copied" augmenter de 8 fois. Concrètement, le code IA-généré ressemble à du code propre, mais il transporte des patterns silencieux que la review humaine n'attrape plus.
Le vendredi dont je parle, la PR portait sur holdSlot : un use case NestJS qui lisait le statut du créneau en base, vérifiait qu'il était AVAILABLE, puis écrivait HELD. Tests verts parce que les tests unitaires mockaient le repository Prisma. Tests d'intégration verts parce qu'ils tournaient en séquentiel. La race condition était invisible jusqu'au jour où deux clients allaient soumettre leur réservation à 50 ms d'écart.
Ce que j'observe sur crmcoaching : je mesure régulièrement mon taux d'acceptation des suggestions Claude sur les PR du projet. Autour de 73% des suggestions sont acceptées telles quelles. Sur les PR que je retravaille, la majorité des retouches concernent le nommage ou le style. Jamais une race condition ou un problème de concurrence ne remonte spontanément. Le filtre humain ne voit plus ce genre de bug, exactement comme le décrit le retour d'expérience sur la code review IA que j'ai publié l'année dernière.
C'est exactement ce que Bruce Schneier appelait "Security is a process, not a product" dans son livre fondateur de 2000. La robustesse d'un holdSlot n'est pas dans le code lui-même, elle est dans la discipline de review qui le précède. Si la discipline tombe, le code ne peut pas la remplacer. C'est la même logique qui anime les principes de clean code et software craftsmanship que je rappelle systématiquement en mission.
Anatomie d'une race condition typique générée par IA
Reprenons le code que Claude m'avait produit pour holdSlot. Voici la version simplifiée qui illustre le pattern :
// apps/api/src/application/slot-hold/hold-slot.use-case.ts
@Injectable()
export class HoldSlotUseCase {
constructor(private readonly prisma: PrismaService) {}
async execute(slotId: string, clientId: string): Promise<void> {
const slot = await this.prisma.slot.findUniqueOrThrow({
where: { id: slotId },
});
if (slot.status !== 'AVAILABLE') {
throw new Error('Slot not available');
}
await this.prisma.slot.update({
where: { id: slotId },
data: { status: 'HELD', heldByClientId: clientId },
});
}
}
Le bug est à deux endroits. D'abord, la lecture de slot.status n'est pas verrouillée : deux requêtes parallèles peuvent toutes les deux lire AVAILABLE et valider la condition indépendamment. Ensuite, l'écriture utilise la valeur lue en mémoire au lieu de demander à Postgres de faire le calcul atomique avec une condition. Résultat : deux clients se retrouvent avec le même créneau marqué HELD.
Pourquoi Claude écrit ce code-là ? Parce que c'est le pattern dominant dans son training data. Sur GitHub, des dizaines de milliers d'exemples Prisma montrent exactement cette séquence findUnique + update. Claude n'a pas conscience que dans un contexte de réservation, la concurrence est la norme et non l'exception. Il code comme un développeur junior bien intentionné qui n'a jamais vu de prod sous charge.
C'est aussi ce que Martin Fowler a documenté dans Refactoring (2018) : le code qui marche dans un test unitaire n'est pas le code qui marche en production. La différence est dans les conditions d'exécution, et l'IA n'a pas accès à ces conditions au moment où elle génère. C'est exactement le genre de risque qui transforme un repo récent en legacy à évaluer urgemment à 12 mois si la discipline de review ne se met pas en place rapidement.
Vous voulez développer le réflexe qui attrape ces bugs avant la prod ?
Repérer une race condition dans une PR Claude, ça ne s'apprend pas en lisant un article : ça se travaille. En mentoring 1:1, je relis votre code et vos PR IA-générées avec vous, et je vous transmets la grille de lecture que j'applique sur crmcoaching. En quelques sessions, vous arrêtez de relire les accolades et vous commencez à voir ce que le modèle ne voit pas.
Le fix craft : transaction atomique et garde explicite
Voici la version que j'ai réécrite avec Claude, en lui donnant le bon contexte cette fois :
// apps/api/src/application/slot-hold/hold-slot.use-case.ts
@Injectable()
export class HoldSlotUseCase {
constructor(private readonly prisma: PrismaService) {}
async execute(slotId: string, clientId: string): Promise<void> {
await this.prisma.$transaction(
async (tx) => {
const result = await tx.slot.updateMany({
where: { id: slotId, status: 'AVAILABLE' },
data: { status: 'HELD', heldByClientId: clientId },
});
if (result.count !== 1) {
throw new SlotAlreadyHeldError(slotId);
}
},
{ isolationLevel: 'Serializable' },
);
}
}
avec l'exception typée dans le domaine :
// apps/api/src/domain/slot-hold/slot-already-held.error.ts
export class SlotAlreadyHeldError extends Error {
constructor(public readonly slotId: string) {
super(`Slot ${slotId} is no longer available`);
this.name = 'SlotAlreadyHeldError';
}
}
Trois changements comptent. D'abord le recours à prisma.$transaction avec isolationLevel: 'Serializable' qui rend l'opération atomique : soit tout passe, soit rien ne passe, et Postgres empêche deux transactions concurrentes de lire le même état incohérent. Ensuite l'updateMany conditionnel (avec where: { status: 'AVAILABLE' }) qui fait vérifier la condition par Postgres au moment de l'écriture, pas par la mémoire Node.js au moment de la lecture. Enfin l'exception typée SlotAlreadyHeldError qui porte le contexte nécessaire aux couches supérieures.
Quand je prompte Claude pour ce fix, je ne dis pas "ajoute une transaction". Je dis : "Ce use case sera appelé en concurrence. Deux clients peuvent réserver le même créneau à 10 ms d'écart. Réécris-le pour qu'il reste correct sous cette contrainte, en utilisant les primitives Prisma adaptées." Le résultat est radicalement meilleur. Claude n'invente pas, il applique. Le craft consiste à lui donner le bon contexte pour qu'il applique le bon pattern, comme le formalise une vraie Definition of Done sur la qualité.
Les 4 garde-fous à appliquer dès lundi
Voici les 4 points que je passe systématiquement en review sur chaque PR Claude qui touche à de l'état partagé (DB, cache, fichier, queue).
Garde-fou 1 : la concurrence est-elle nommée explicitement ? Pour toute fonction qui lit puis écrit, je me pose la question : "Ce use case peut-il être appelé par 2 requêtes simultanées ? Si oui, que se passe-t-il ?" Si la réponse est "je ne sais pas", la PR retourne en draft.
Garde-fou 2 : les écritures qui dépendent de lectures passent-elles par une transaction ? Pas de read-modify-write hors prisma.$transaction. C'est la règle. Si Claude n'a pas utilisé une transaction avec un niveau d'isolation explicite, c'est un signal qu'il a écrit le pattern naïf.
Garde-fou 3 : les exceptions sont-elles typées et porteuses de contexte ? throw new Error('Slot not available') est insuffisant. throw new SlotAlreadyHeldError(slotId) permet à la couche supérieure de prendre des décisions différenciées, et à l'observabilité de remonter les bons signaux. Ce détail est ce qui sépare le code junior du code senior, et c'est aussi ce qui permet de construire les patterns de résilience type circuit breaker et retry en aval.
Garde-fou 4 : un test de concurrence existe-t-il ? Pour toute méthode critique, j'exige un test Vitest qui soumet le use case en parallèle via Promise.all sur le même créneau, et qui vérifie que l'état final est cohérent (un seul HELD, une seule erreur SlotAlreadyHeldError). Si ce test n'existe pas, Claude ne l'a pas écrit. C'est à la review de l'exiger.
// Exemple de test de concurrence Vitest
it('ne doit accorder le créneau qu'à un seul client en concurrence', async () => {
const slotId = 'slot-test-id';
const results = await Promise.allSettled([
holdSlotUseCase.execute(slotId, 'client-A'),
holdSlotUseCase.execute(slotId, 'client-B'),
]);
const fulfilled = results.filter((r) => r.status === 'fulfilled');
const rejected = results.filter((r) => r.status === 'rejected');
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
expect((rejected[0] as PromiseRejectedResult).reason).toBeInstanceOf(SlotAlreadyHeldError);
});
Ces 4 garde-fous tiennent en 2 minutes par PR. Ils attrapent 90% des bugs de concurrence que j'ai vus passer en prod sur des codebases IA-heavy. Le coût d'application est bas. Le coût d'omission peut, lui, se chiffrer en heures de débogage sur un incident un vendredi soir.
Ce que ça change concrètement
Sur les 3 dernières missions où j'ai déployé cette grille de review, voici les chiffres avant/après mesurés sur 4 mois.
| Métrique | Avant grille | Après 4 mois |
|---|---|---|
| Bugs de concurrence trouvés en review | 2 par mois en moyenne | 11 par mois en moyenne |
| Bugs de concurrence remontés depuis la prod | 5 par trimestre | 1 par trimestre |
| Temps moyen de review d'une PR Claude | 8 minutes | 11 minutes |
| Confiance équipe dans le code IA-généré (sondage 1-10) | 6,2 | 8,1 |
Le coût est de 3 minutes supplémentaires par PR. Le gain est de 4 bugs de prod évités par trimestre. Sur une équipe de 5 développeurs avec 60 PR par mois, c'est environ 3 heures par semaine investies pour éviter 8 à 12 incidents par an. Le rapport est nettement favorable. C'est aussi ce qu'une revue de code structurée et appliquée à l'ère IA permet d'enclencher : pas un contrôle qualité figé, mais un filet adaptatif qui suit là où l'IA produit ses angles morts.
Ces 4 garde-fous ne sont qu'un début : il en existe 100
La grille de review concurrence décrite ici fait partie d'un référentiel plus large : le Craft Bundle, les 100 pratiques craft que j'applique pour écrire du code propre et robuste, celles que l'IA ne vous apprendra jamais parce qu'elle ne les a jamais vues tourner en prod. De quoi transformer vos réflexes, une pratique à la fois.
Conclusion
Ce que je veux que vous reteniez de cet article, c'est que Claude n'écrit pas de bugs au sens où on l'entendait avant. Il écrit du code qui passe les tests qu'on lui demande de passer, dans les conditions qu'on lui décrit. Quand ces conditions ne couvrent pas la concurrence, la latence réseau, les partial failures, le code est silencieusement faux. Le bug est là, mais il dort.
Votre rôle de senior, en 2026, n'est pas de relire les accolades. C'est de poser à chaque PR Claude les 4 questions que l'IA ne se pose pas elle-même. Si vous tenez cette discipline, vous transformez Claude d'un junior risqué en un copilote fiable. Si vous la lâchez, vous accumulez de la dette qui se révèlera un vendredi soir, quand il ne faudra surtout pas qu'elle se révèle.
Si en lisant ces lignes vous reconnaissez votre situation, vous avez deux choix. Vous pouvez attendre votre prochain incident pour réagir. Ou vous pouvez commencer lundi matin, par une grille de review en 4 points, et apprendre à voir ce que Claude vous fait coder sans que vous le sachiez.
Pour la suite des patterns craft + IA en format court, retrouvez-moi sur mon profil Instagram kamangacode, où je publie chaque semaine les pièges que je vois revenir en mission.
FAQ sur les bugs Claude et la review IA-générée
1. Comment justifier auprès de mon équipe le temps supplémentaire de review ?
Le calcul est simple. 3 minutes de review supplémentaires par PR, sur 60 PR par mois, c'est 3 heures par mois. Un seul incident de race condition en prod sur une fonctionnalité critique peut coûter entre 5 000 et 50 000 euros en remédiation, perte de confiance client et heures de war room. Le ROI est calculable et défendable devant n'importe quel CTO, et c'est exactement l'angle que je détaille dans l'ingénierie logicielle comme avantage concurrentiel.
2. Faut-il refuser systématiquement les PR Claude qui touchent à l'état partagé ?
Non, ce serait contre-productif et casserait la vélocité. Ce qu'il faut, c'est instrumenter la review avec les 4 garde-fous décrits ici, et exiger explicitement que Claude soit re-prompté avec le contexte de concurrence quand il manque. Le test : si vous ne savez pas répondre à "que se passe-t-il si 2 appels simultanés", la PR retourne en draft.
3. Les tests automatisés ne devraient-ils pas attraper ces bugs ?
En théorie oui, en pratique non. Les tests unitaires mockent la DB, donc ils ne voient pas la concurrence. Les tests d'intégration tournent en séquentiel par défaut, donc ils ne déclenchent pas la race. Il faut écrire des tests de concurrence explicites (via Promise.all sur la même ressource en Vitest), et ces tests, Claude ne les écrit jamais spontanément. C'est aussi ce que pointe la checklist pour tester du code généré par IA.
4. Quels autres patterns dangereux faut-il surveiller dans le code Claude ?
J'en ai documenté 5 récurrents : la God Function, le couplage métier/ORM, les tests qui ne testent rien, le catch-all silencieux et les dépendances non auditées. Chacun mérite un traitement spécifique en review. Le sujet est aussi lié aux vulnérabilités de sécurité dans le code LLM-généré pour les patterns les plus sensibles.
5. Comment former mon équipe à voir ces bugs sans alourdir la charge cognitive ?
La meilleure méthode que j'ai vue fonctionner, c'est de transformer la grille en 4 garde-fous en checklist GitHub PR template, visible à chaque review. Les juniors la suivent par discipline, les seniors la suivent par réflexe, et au bout de 3 mois tout le monde l'a intégrée sans s'en rendre compte. Le rituel s'inscrit dans une culture engineering de rituels craft qui transforme les bonnes intentions en habitudes durables.
6. Est-ce que ce problème va disparaître avec les prochaines versions de Claude ?
Non, et c'est important de le comprendre. Le problème n'est pas dans la qualité du modèle, il est dans l'absence de contexte d'exécution au moment de la génération. Tant que Claude génère du code sans observer la prod réelle (charge, concurrence, latence), il continuera à produire des patterns qui passent les tests et plantent sous contrainte. L'amélioration viendra des outils d'exécution autour de l'IA, pas du modèle lui-même.
Ressource gratuite : Engineering Maturity Assessment
L'EMA est l'outil que je propose au début de chaque mission. Il mesure la maturité de votre équipe sur plusieurs axes engineering, dont la code review, la gouvernance IA et la résilience prod. Quelques minutes pour identifier où votre filet de review craque le plus, et où concentrer vos efforts en priorité.


