veilletech.fr
31 août Feed du jour
#07 DEVOPS Article

Le echo qui avalait tous vos codes de sortie

Un journal vert ne prouve que l'existence du vert.

Une ligne de journal du type echo "[$(date)] done (exit $?)" ne peut jamais rapporter autre chose que zéro : la substitution de commande est évaluée avant $? et écrase le code de sortie. L'auteur a passé 28 heures avec des journaux entièrement verts pendant qu'aucun de ses 171 jobs launchd ne faisait son travail.

3 min de lecturevidéo 1:12
Partager
Sommaire4 sections
  1. Ce qui se passe
  2. Pourquoi ça compte
  3. Comment s'y prendre
  4. À retenir

Ce qui se passe

Pendant vingt-huit heures, un environnement d'automatisation reposant sur 171 tâches launchd a affiché (exit 0) sur chaque ligne de journal. Aucune alerte, aucun rouge. Et aucun travail effectué : les réponses attendues sur les plateformes cibles étaient à zéro. Les échecs avaient bien lieu. Les codes de sortie ne remontaient simplement jamais à l'appelant.

Le coupable était la ligne de journalisation elle-même :

Terminal
node "$SCRIPT" "$@"
echo "[$(date '+%F %T')] $LANE done (exit $?)"

Les arguments de echo sont évalués de gauche à droite. La substitution de commande $(date ...) lance un sous-shell, ce sous-shell réussit, et $? vaut désormais 0. La lecture de $? a lieu après. Cette ligne est syntaxiquement incapable d'écrire autre chose que exit 0, quel que soit le sort de node. L'auteur l'a confirmé par un test de reproduction : avant correction, un exit 4 s'affichait en 0 ; après, en 4.

Pourquoi ça compte

L'effet est rétroactif, et c'est le plus dérangeant. Tant que la ligne est en place, aucun (exit 0) du mois écoulé ne vaut comme preuve. « C'était vert la semaine dernière aussi » signifie seulement qu'on a continué à exécuter du code qui affiche du vert.

Le contexte aggrave la portée : ces scripts servaient de garde-fous branchés sur les hooks PreToolUse et Stop de Claude Code, dont la spécification veut qu'un exit 1 bloque l'appel d'outil ou la fin de réponse. Un garde-fou qui ne sait pas transmettre son exit 1 ne bloque rien du tout — c'est une rambarde peinte sur le vide.

L'auteur documente deux variantes du même piège, à chercher dans son propre outillage :

Situation Ce qui casse
Enrobage JavaScript appelant le script via child_process.exec() Avec shell: true, le shell intermédiaire peut absorber l'exit 1, qui n'atteint jamais le processus parent
Script en bout de tube, cat entree.json | ./hook.sh Sans set -o pipefail, seul le code du membre de droite remonte

À noter que set -euo pipefail était bien présent en tête du script incriminé. Il protégeait l'intérieur de ce fichier — le problème était en dehors, dans la chaîne qui l'appelait.

Comment s'y prendre

La correction est mécanique : capturer le code immédiatement, sur la ligne qui suit la commande, avant tout autre appel.

Terminal
node "$SCRIPT" "$@"
rc=$?
echo "[$(date '+%F %T')] $LANE done (exit $rc)"
exit $rc

Puis passer son outillage au peigne. Une recherche suffit à sortir les candidats :

Terminal
grep -rn 'echo .*\$(.*\$?' scripts/

Enfin, la seule vérification qui vaut : faire échouer volontairement une étape et regarder si le journal l'affiche. Un garde-fou non testé en conditions d'échec n'a jamais été testé.

Source : DEV — 28 Hours of Green Logs, Zero Replies