Mon App IA Plantait sur un MacBook à 4 000$ — Le Code Était Pourtant Parfait

7 min read

Mon app plante. Crash aléatoire, le pire genre. Elle meurt comme un poisson rouge qu'on retrouve le ventre en l'air au réveil, sans la moindre explication. Quinze minutes de build sur un MacBook à 4 000 €. Le code compile. Les tests passent. Et pourtant : next: command not found.

Pendant deux jours, Claude a accusé Next.js, Turbopack, Node, npm, la météo. J'ai lu les logs comme un moine médiéval recopiant les écritures. Rien. Le coupable se cachait dans macOS lui-même, dans un truc auquel aucun développeur ne pense : iCloud Drive. Chaque tentative de correction empirait le problème de façon exponentielle.

TLDR : Si vos projets vivent dans ~/Documents/, iCloud traite node_modules comme des photos de vacances et vire les fichiers au hasard. Chaque "solution" intelligente empire tout. Le vrai fix tient en une commande. Cet article vous épargne deux jours.

Et ce qui fait mal : mon process de dev est solide. Je développe avec des contrats de prompt qui transforment le code IA du casino au shipping. Le code était testé, structuré, documenté. Ce qui signifiait que le problème devait se cacher complètement ailleurs.

Deux développeurs dépannant une panne logicielle, l'un frustré et l'autre triomphant
Quand votre MacBook à 4k€ devient un presse-papier très cher 🔥💻

L'App Qui Refusait de Tourner

Ça a commencé un mardi. Le serveur de dev Next.js bootait, compilait un moment, puis crevait. Parfois après 30 secondes. Parfois après 10 minutes. Parfois il tournait nickel pendant une heure puis plantait en plein hot reload.

Aucun pattern. Aucun déclencheur reproductible. Juste la mort.

Turbopack se plaignait de modules manquants. Des fichiers qui existaient clairement selon ls disparaissaient en pleine compilation. Je redémarrais, tout marchait, puis ça replantait en testant un composant.

J'ai fait ce que fait tout développeur : j'ai accusé le framework. Vidé .next. Supprimé node_modules, lancé npm install. Vérifié la config Turbopack. Downgradé. Upgradé. Rien n'aidait de façon consistante.

Puis, par pur désespoir, j'ai fermé VS Code et ouvert Finder.

Le bug n'était pas dans le code. Il n'était même pas dans le terminal.

L'Expulsion Invisible

Petites icônes de nuage. Sur les fichiers dans node_modules.

<figure data-source="image">
iCloud Drive placeholders replacing node_modules dependencies in Finder

Les icônes de nuage d'iCloud Drive remplaçant les vrais fichiers de dépendances JavaScript.

iCloud Drive expulsait les fichiers JavaScript de mes dépendances. Supprimait le vrai code source du disque et le remplaçait par des placeholders cloud pour "optimiser le stockage". Mon dossier node_modules traité comme une galerie de photos de vacances : uploadé, puis nettoyé localement parce que sûrement je n'avais pas besoin de tous ces fichiers sur ma machine.

Pourquoi ? Mes projets vivaient dans ~/Documents/dev/. Sur macOS, ~/Documents/ est synchronisé avec iCloud Drive par défaut. Tous les fichiers là-dedans sont du gibier.

Le fix connu, c'est le pattern de suffixe .nosync :

mv node_modules node_modules.nosync
ln -s node_modules.nosync node_modules

Renommer le vrai dossier pour qu'iCloud l'ignore, créer un symlink pour que Node.js le trouve toujours au bon endroit.

J'ai appliqué le fix. Serveur redémarré. Dashboard chargé.

Problème résolu. Le lendemain matin, tout était cassé à nouveau.

La Descente : Cinq Fixes Qui Ont Tout Empiré

Café du matin. J'ouvre le terminal. next: command not found. Le symlink avait disparu. Pas le dossier, le dossier était bien là, intact. Juste le symlink qui s'était volatilisé.

Je l'ai recréé. Serveur booté. Quinze secondes plus tard : crash. Symlink disparu.

C'est là que j'ai remarqué :

$ ls | grep node_modules
node_modules 3
node_modules 4
node_modules 5
node_modules 6
node_modules 7
node_modules 8
node_modules.nosync

iCloud ne supprimait pas mon symlink. Il le renommait. Conflit détecté avec un fantôme côté serveur, résolu en collant un numéro sur la version locale. En fin de journée : node_modules 20.

Ce qui suit, ce sont cinq tentatives de correction. Chacune plus maligne que la précédente. Chacune empirant les choses. Une vraie descente aux enfers, comme ces histoires d'horreur où chaque porte que vous ouvrez mène à une pièce plus petite.

Fix 1 : Nettoyer les copies numérotées. Évidemment les doublons alimentent la boucle. Les supprimer, le problème disparaît. Ça a marché 20 secondes. Puis node_modules 9 est apparu. L'entrée côté serveur était toujours là.

Fix 2 : Attributs étendus pour exclure de la sync. macOS a des attributs étendus (xattrs) qui contrôlent comment le file provider gère les fichiers. J'ai marqué le symlink comme exclu :

xattr -w com.apple.bird.metadata '{"excludeFromSync":true}' node_modules
xattr -w com.apple.fileprovider.ignore 1 node_modules

C'est le fix qui a vraiment tout cassé. Au lieu de disparitions aléatoires, le symlink disparaissait maintenant en 15 secondes chrono. À. Chaque. Fois. Comme si j'avais peint une cible dessus.

Fix 3 : Flag immutable. chflags uchg rend un fichier non-supprimable. Sûrement rien ne peut outrepasser le système de fichiers lui-même ?

Le daemon file provider d'iCloud tourne avec des privilèges élevés. Il a contourné le flag immutable, créé node_modules 19, supprimé l'original "protégé". (Merci bien.)

Fix 4 : Déplacer node_modules complètement hors d'iCloud. Mettre les vrais fichiers dans ~/Library/ (pas synchronisé), le symlink pointe là-bas. Logique.

Ça plantait toujours. iCloud se fiche de l'endroit où pointe un symlink. Il gère le symlink lui-même parce que le symlink vit dans un répertoire synchronisé. Cible sûre, lien dangereux.

Fix 5 : Script watchdog. Une boucle bash recréant le symlink toutes les 2 secondes. Sparadrap. Turbopack lance FATAL quand node_modules disparaît en pleine compilation. Deux secondes, c'est une éternité pour un bundler.

Chaque fix "intelligent" donnait à iCloud une raison de plus d'intervenir.

<figure data-source="infographic">
iCloud Symlink Feedback Loops

Boucles de Feedback des Symlinks iCloud

Le Projet d'À Côté Marchait Très Bien

Après avoir épuisé toutes les idées malignes, j'ai fait ce que j'aurais dû faire en premier : comparer mon projet cassé avec un qui marchait.

Un autre projet traînait dans le même exact dossier ~/Documents/dev/. Même pattern .nosync + symlink. Jamais eu un seul conflit.

J'ai lancé xattr -l sur les deux :

com.apple.fileprovider.dir#N: 1
com.apple.provenance:

com.apple.bird.metadata: {"excludeFromSync":true}
com.apple.fileprovider.dir#N: 1
com.apple.fileprovider.ignore: 1
com.apple.provenance:

Deux attributs en plus. com.apple.bird.metadata et com.apple.fileprovider.ignore. Exactement ceux que j'avais ajoutés dans le Fix 2.

Le projet sain n'avait rien de spécial. Il n'avait juste pas été "protégé".

Une Commande : xattr -c node_modules

xattr -c node_modules

Vider TOUS les attributs étendus personnalisés. Laisser iCloud traiter le symlink comme un fichier banal et sans intérêt.

Mais vider les xattrs seuls n'était pas la fin. Le serveur iCloud avait toujours le répertoire node_modules original d'avant la config .nosync. Trente minutes plus tard, il a téléchargé cette vieille copie et écrasé mon symlink. Le fantôme était toujours sur le serveur.

La purge complète :

mv node_modules node_modules_purge.nosync

ls -la node_modules   # devrait dire "No such file"

ln -s node_modules.nosync node_modules

rm -rf node_modules_purge.nosync

L'attente compte. La zapper et iCloud n'a pas encore synchronisé la suppression. Votre symlink frais entre en conflit avec la copie serveur. Retour à la boucle.

Après la purge complète : le serveur a tenu. Cinq minutes. Trente minutes. Une heure.

Voici le mécanisme (Apple ne documentera jamais ça, donc c'est du reverse-engineering comportemental) : le file provider d'iCloud surveille les répertoires synchronisés. Un fichier avec des xattrs non-standard est marqué comme "modifié localement". Ça déclenche un cycle de sync comparant local vs. serveur. Si le serveur a une entrée différente avec le même nom, conflit détecté, fichier local renommé. Le fichier renommé porte toujours les xattrs personnalisés, ce qui déclenche une autre sync. Boucle. Sans xattrs personnalisés, iCloud voit le symlink comme inchangé et l'ignore complètement.

La meilleure protection contre iCloud, c'est de ne pas attirer son attention.

Votre App IA N'Est Que Aussi Bonne Que l'OS En Dessous

Voici ce qui me tue. On obsède sur le prompt engineering, la sélection de modèles, les benchmarks de frameworks. On compare Turbopack vs. Webpack comme si c'était une guerre sainte. Et pendant ce temps l'OS en dessous expulse discrètement nos dépendances parce qu'il pense que node_modules c'est un album photo.

Les apps IA sont plus exposées à ça que les apps web classiques. Plus de fichiers (les agents ont besoin d'outils, d'embeddings, de configs de modèles). Plus de churn filesystem (cycles de hot reload, régénération de cache). Plus de trucs qui ressemblent à "gros dossiers de bordel qu'iCloud devrait optimiser". Si votre projet IA a node_modules plus un vector store plus des artefacts de modèles, iCloud voit une mise en place de fichiers à gérer serviablement pour vous.

Cinq règles qui m'auraient épargné deux jours :

Toujours .nosync + symlink pour node_modules. L'automatiser. Un wrapper dans .zshrc qui tourne après chaque npm install, renomme le dossier, recrée le lien. Non-négociable si vos projets sont dans ~/Documents/.

Jamais toucher aux xattrs dans les répertoires iCloud. Pas de com.apple.bird.metadata. Pas de com.apple.fileprovider.ignore. Pas d'attributs malins. Ils ne protègent pas les fichiers d'iCloud. Ils peignent une cible dessus.

Quand rm -rf traîne, utiliser mv avec .nosync. mv node_modules old_nm.nosync est instantané sur APFS. Sans .nosync dans le nouveau nom, iCloud commence à uploader le dossier renommé. Des centaines de mégas de node_modules synchronisés pour rien. (Et jamais mv vers /tmp, volume différent, ça déclenche une copie.)

Garder .next comme vrai répertoire. Turbopack ne peut pas gérer les symlinks pour son cache. Si iCloud crée .next 2 ou .next 3, supprimer les copies numérotées, ne pas symlinker.

Ou juste sortir d'iCloud. mv ~/Documents/dev ~/dev. Ce chemin n'est pas synchronisé. Option nucléaire, fix permanent. Probablement ce que j'aurais dû faire dès le début au lieu de passer deux jours à jouer au chat et à la souris avec un daemon système.


Deux jours dans les choux, le genre où on a envie de balancer son laptop dans la piscine. Et je suis assis au soleil, WiFi qui rampe comme un chien à trois pattes sous sédatifs.

Le code était solide. Le framework configuré correctement. Ça marchait hier (le fameux "ça marchait hier"). Un service système conçu pour synchroniser les pathétiques photos de vacances de gens qui pensent que "le cloud" c'est du stockage magique. Pas pour le dev. Définitivement pas pour le dev. Et Apple avec leur secret compulsif sur comment tout ça marche sous le capot...

Le fix tenait en une commande. Mais Claude Code ne l'a trouvé qu'après avoir brûlé cinq "solutions" qui ont tout empiré. Chaque tentative de protéger mes fichiers donnait à iCloud une raison de plus de les attaquer.

Si votre app tourne sur Mac et que vos projets traînent dans ~/Documents/, vérifiez. Pas votre code. Pas vos deps. Vos attributs étendus. Parce que macOS n'a pas été conçu pour vous. Il a été conçu pour Karen de la Compta.

PS : J'ai désactivé ma propre sync de photos de vacances il y a longtemps. Je dis ça comme ça. 😏


Sources : La documentation File Provider d'Apple (qui ne dit commodément rien sur le comportement de conflit xattr). Le pattern .nosync est documenté par la communauté mais pas officiellement supporté par Apple.

(*) La couverture est générée par IA. iCloud était occupé à expulser la vraie. 🤷