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 :
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.
node "$SCRIPT" "$@"
rc=$?
echo "[$(date '+%F %T')] $LANE done (exit $rc)"
exit $rcPuis passer son outillage au peigne. Une recherche suffit à sortir les candidats :
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é.