Retour d'expérience · Claude Code
Longs travaux d'agents Claude Code : pourquoi on perd du travail à la coupure, et comment ne plus rien perdre
Pour ton Claude
Le même texte en Markdown, plus simple à lire pour un agent : rapport.md. Donne-lui l'adresse
https://agents-sans-perte.pages.dev/rapport.md et demande-lui de challenger le constat et la méthode, en revérifiant les faits marqués
doc sur sa propre version de Claude Code.
Niveaux de preuve : doc documentation officielle, relue le 05/10/2026 · observé nos journaux et relevés · hypothèse non prouvé par un test.
À lire comme : un constat suivi d'une méthode, à challenger. Chaque affirmation porte son niveau de preuve :
- doc : lu dans la documentation officielle (code.claude.com/docs), le 05/10/2026 ;
- observé : constaté sur nos journaux ou nos relevés
/usage; - hypothèse : notre meilleure explication, non prouvée par un test.
Claude Code bouge presque chaque jour : tout ce qui est marqué doc est à revérifier sur ta version.
1. L'incident : trois agents d'une demi-heure chacun, rien sur le disque
Un workflow (outil Workflow, scripts JS qui orchestrent des sous-agents) devait collecter puis vérifier quelques milliers de chiffres sur le web pour une étude de marché.
- La collecte s'est bien passée. Douze agents Opus 5.5, dix en parallèle, ont écrit leurs fichiers en cours de route. Le run a été mis en pause à la main à la frontière entre collecte et vérification, puis repris dans la même conversation : la collecte est revenue du cache sans rien recoûter. observé
- La vérification a vidé ce qui restait de la fenêtre de 5 h en environ 35 minutes. Quatre vérificateurs ont démarré en parallèle sur Fable 5.1, le modèle le plus cher, chacun chargé de quatre ou cinq fichiers. observé
- À la coupure, un seul vérificateur avait fini. Les trois autres avaient travaillé une demi-heure et n'avaient presque rien écrit : deux fichiers absents, une ébauche de 423 octets. Leur consigne disait pourtant « écris au fur et à mesure ». observé
- Puis 23 appels ont échoué à la suite. Corrections, tours suivants, consolidation : le script a continué à lancer des agents qui échouaient aussitôt. observé
- Bilan : environ les trois quarts d'une fenêtre de forfait perdus. observé
2. Ce que la mécanique de Claude Code fait réellement
2.1 La limite de 5 h est une limite de session, tous modèles confondus
- Passer les agents sur un autre modèle ne contourne pas la limite. Des agents basculés sur Fable ont échoué sur « You've hit your session limit ». observé, septembre 2026
- Le plafond propre à Fable est un plafond hebdomadaire de plus, pas une réserve à part. observé
- Seul
/usagefait foi, et Claude ne peut pas le lire lui-même. Un script maison qui estimait la consommation en additionnant les tokens notés dans les journaux d'agents (agent-*.jsonl) s'est trompé d'un facteur 3 environ. observé
2.2 La pause à la limite est active par défaut, sauf dans certaines sessions
- Depuis la v2.1.234, une session interactive sur abonnement claude.ai attend la remise à zéro, puis continue seule. Le réglage
autoContinueAtUsageLimitest actif sans qu'on le pose. Il se réarme deux fois de suite au plus. On l'annule parÉchap, ou par/rate-limit-options, puis « Don't continue automatically ». doc,interactive-mode, « Wait for a usage limit to reset » - Depuis la v2.1.271, un workflow fait de même. Les agents touchés attendent, aucun nouvel agent ne démarre, puis le run repart seul. doc,
workflows, « When a run hits your usage limit » - Cinq cas où la pause ne se fait pas, et où les agents échouent doc, même section :
- une session Remote Control, c'est-à-dire une session locale qu'on peut piloter depuis le téléphone ou le navigateur (réglage
remoteControlAtStartup, ouclaude remote-control) ; - une session en arrière-plan ;
claude -pet l'Agent SDK ;- une remise à zéro à plus de 24 h, typiquement une limite hebdomadaire ;
- une troisième limite d'affilée.
- une session Remote Control, c'est-à-dire une session locale qu'on peut piloter depuis le téléphone ou le navigateur (réglage
Ce que ça ne dit pas : si un agent en pause garde son fil de travail, ou s'il repart de zéro à la reprise. La doc dit seulement que les agents en attente « repartent ».
2.3 Reprendre un workflow rejoue tout ce qui a démarré après un échec
resumeFromRunId relance un run dans la même conversation, ou dans une conversation rouverte par claude --resume. doc, workflows, « Resume after a pause » Le rejeu suit l'ordre de démarrage des agents :
- un agent fini revient du cache ;
- le premier agent modifié ou échoué repart, et tous ceux qui ont démarré après lui aussi, même finis. Exemple de la doc : A, B, C, D démarrent dans cet ordre et B échoue ; à la relance, A revient du cache, B, C et D repartent ;
- arrêter le run entier ne compte aucun agent comme échoué ; arrêter un seul agent (bouton Stop de sa carte) compte comme un échec ;
- un agent en vol à l'arrêt repart de zéro ;
- une relance est refusée tant que des agents de l'ancien run tournent. Un run neuf, lui, n'est pas mentionné.
Conséquence : le cache d'un workflow est un bonus, jamais une garantie.
2.4 Un script de workflow est aveugle
- Il n'a ni disque, ni module, ni horloge.
Date.now()etnew Date()lèvent une erreur. Il ne peut donc pas savoir ce qui est déjà fait : il faut le lui dire parargs. doc, « Behavior and limits » agent()rendnullaprès un échec définitif, et peut aussi lever une exception, par exemple sur un schéma de sortie raté cinq fois. docagent()acceptemodeleteffortpar appel. doc- Concurrence : jusqu'à 16 agents, moins sur une machine qui a moins de processeurs. Nous en avons observé 10 sur un i7 à 12 fils d'exécution. Réglable par
CLAUDE_CODE_WORKFLOW_MAX_CONCURRENT_AGENTS, v2.1.269 et suivantes. doc et observé
2.5 Permissions : un agent peut bloquer tout le run
- Les règles
askdesettings.jsonpriment sur le mode auto. Un agent qui voulait vider une variable d'environnement parRemove-Item Env:Xa attendu un clic humain. observé - Une demande de permission d'un agent met tout le workflow en pause. doc
- Les alias PowerShell sont ramenés à leur cmdlet avant les règles :
deletridéclenchent une règleRemove-Item. doc,permissions - Les crochets des fichiers de réglages s'appliquent aussi aux outils des sous-agents. Leur entrée porte
agent_idetagent_type, ce qui permet de viser les seuls sous-agents. doc,hooks - Un crochet
PreToolUsequi sort en code 2 bloque l'appel avant l'évaluation des règles de permission. Une décision JSONallowouask, elle, ne contourne pas une règleask. doc,permissions, « Extend permissions with hooks »
2.6 Chaque agent coûte un ticket d'entrée
Chaque sous-agent recharge les CLAUDE.md du workspace et du projet avant de commencer. Chez nous, avec un gros profil personnel importé, c'est environ 165 Ko, soit ~45 000 tokens. observé, tailles de fichiers ; le chiffre de tokens est une estimation Le cache de prompt partagé entre agents d'un même run atténue ce coût, sans le supprimer. doc, « Prompt caching in a fan-out »
Conséquence : émietter un travail en très petites tâches n'est pas gratuit.
2.7 Le quota de recherches web se partage
Plusieurs agents d'une même session ont buté sur un « quota de 200 recherches ». La session principale cherchait encore pendant ce temps : c'est plutôt une limite de débit qu'un plafond dur. observé Parades qui ont marché : lire directement les adresses connues (WebFetch), ou passer par DuckDuckGo Lite et Startpage dans un Chrome invisible piloté par Playwright.
3. Le diagnostic : les agents ont échoué à cause de Remote Control, et perdu leur travail à cause du format de sortie
- La coupure : sans doute Remote Control. hypothèse forte La session avait été ouverte avec
remoteControlAtStartup: true; son journal contient des enregistrementsbridge-session, et la doc exclut les sessions Remote Control de la pause d'un workflow (§2.2). La limite touchée était celle de 5 h (rateLimitType: five_hour), touchée vers 00:55, avec une remise à zéro vers 3 h du matin : sans Remote Control, le run aurait dû attendre deux heures, puis repartir. Non prouvé par un test. - La perte : le format de sortie rendait la consigne impossible à tenir. observé Chaque vérificateur devait remplir un seul fichier JSON contenant un tableau de verdicts. Pour l'enrichir, il faut le réécrire en entier, chaque fois plus gros, donc plus cher : l'agent attend logiquement la fin. Le seul vérificateur qui a fini a écrit ses 110 Ko d'un coup, à la dernière minute.
- La cascade : le script avalait les échecs. observé, en relisant le script
- Une fonction de repli relançait chaque échec sur un autre modèle, ce qui a doublé les appels ratés, sans aucune chance de succès puisque la limite est commune à tous les modèles.
- Une boucle de correction, faute de résultat, gardait l'ancienne liste de contestations et relançait corrections et tours suivants dans le vide.
- Ces 23 échecs n'ont presque rien coûté déduit : ils tombaient aussitôt sur la limite. Ce qui a coûté, ce sont les trois agents en vol.
- L'exposition : trop gros, trop cher, trop à la fois. observé Quatre agents sur le modèle le plus cher, chacun avec quatre ou cinq fichiers, soit 30 à 60 minutes de travail par agent. Fable a coûté jusqu'à environ cinq fois un agent Opus par minute (§7). Le parallélisme ne change pas le coût total, mais il accélère la course vers la limite et met plus de travail en vol au moment où elle tombe.
4. La méthode : neuf règles
Le principe : le disque est la seule mémoire qui compte. Ni la mémoire d'un agent, ni le cache d'un workflow, ni la conversation ne survivent sûrement à un arrêt. Un arrêt peut venir de la limite, d'un plantage, d'une coupure de courant, de VS Code qu'on ferme, ou d'une demande de permission sans réponse.
Quand l'appliquer en entier : tout workflow, tout lancement de plus de trois agents, tout travail d'agents estimé à plus d'une demi-heure. En dessous, le bloc de reprise (§5.1) et les règles 6 à 9 suffisent.
- Une tâche = une sortie. Chaque agent produit un résultat qui tient seul (un fichier à vérifier, pas cinq), en 10 à 20 minutes. Pas plus fin, à cause du ticket d'entrée (§2.6). Une entrée trop grosse pour un agent (plusieurs mégaoctets) se prépare d'abord par un script, qui en extrait une table compacte.
- On écrit par lots, dans des fichiers neufs. Tous les 15 à 30 résultats, un nouveau fichier :
lot-001.json,lot-002.json… Un lot écrit ne se touche plus. On écrit avec l'outil Write, pas par une redirection du shell : on évite ainsi les pièges de guillemets de PowerShell et les demandes de permission. - Une marque de fin. L'agent qui a fini écrit
FIN.json. SansFIN.jsonlisible, la tâche est inachevée. - L'agent sait reprendre. Sa consigne lui dit de lire ses lots existants et de continuer après (§5.1).
- La liste des tâches est sur le disque avant le lancement (
taches.json, §5.2). L'état de chaque tâche se calcule en regardant son dossier ; on ne le tient pas à la main. - Un échec arrête les lancements. Pas de nouvel essai automatique, pas de repli sur un autre modèle, pas d'étape suivante bâtie sur un résultat manquant.
- Trois agents à la fois au plus, sauf décision explicite, réglés à chaque run.
- Le modèle le plus cher seulement pour juger : synthèse, arbitrage, avocat du diable. Collecter, vérifier un chiffre contre sa source, consolider : Opus ou Sonnet.
- Rien qui attende un humain. Un sous-agent ne supprime, ne déplace ni ne renomme rien, et un crochet le lui refuse (§5.4).
Deux habitudes complètent ces règles :
- une étape = un workflow (collecter, vérifier, corriger, consolider), pour voir le résultat et la consommation entre deux ;
- ce qui est mécanique se fait par script, sans agent : assembler les lots, consolider des statuts.
5. Les outils : bloc de reprise, liste des tâches, squelette de workflow, crochet
5.1 Le bloc de reprise, à coller dans chaque consigne d'agent
## Écriture et reprise (obligatoire)
Ton dossier de sortie : <DOSSIER ABSOLU>
1. Avant tout, regarde ce dossier. S'il contient FIN.json, ton travail est déjà fini : réponds avec
son contenu, sans rien refaire. S'il contient des lots (lot-001.json, lot-002.json…), lis-les :
ce qu'ils contiennent est fait, ne le refais pas, continue après.
2. Écris tes résultats par lots, chaque fois dans un fichier NEUF (lot-001.json, puis
lot-002.json…), avec l'outil Write, dès que tu as <N> résultats. Ne réécris jamais un lot déjà
écrit et ne le complète pas : un lot écrit est figé. Ne garde rien en tête que tu n'aies pas
encore écrit.
3. Quand tout est fait, écris FIN.json : { "termine": true, "lots": <nombre de lots>,
"resultats": <nombre de résultats>, "remarques": "" }. Sans FIN.json, ton travail sera
considéré comme inachevé et repris.
4. Tu ne supprimes, ne déplaces ni ne renommes rien. Une variable d'environnement se vide par
$env:NOM = $null. Tes brouillons vont dans <DOSSIER>brouillon/ et y restent. Si une suppression
te semble nécessaire, dis-le dans ta réponse.Le seuil est un nombre de résultats, pas une durée : un agent n'a pas d'horloge.
5.2 La liste des tâches
{
"chantier": "vérification des chiffres collectés",
"cree": "2026-10-05",
"taches": [
{
"id": "verif-fichier-a",
"sortie": "/chemin/absolu/verification/fichier-a/",
"entrees": ["/chemin/absolu/collecte/fichier-a.json"],
"consigne": "consignes/verif-fichier-a.md",
"modele": "opus",
"effort": "high"
}
]
}L'état d'une tâche se lit dans son dossier :
- ni
lot-*.jsonniFIN.json: à faire ; - des lots sans
FIN.json: entamée ; FIN.jsonlisible : faite.
Il n'y a volontairement pas de statut « en cours » : après un plantage, personne ne le remettrait à jour. La consigne de chaque tâche y figure, pour qu'une conversation neuve relance exactement la même.
5.3 Le squelette d'un workflow : seulement ce qui manque, trois à la fois, arrêt au premier échec
La session fait l'inventaire du disque, puis passe au script les tâches restantes et la date. agent, phase, log et args sont fournis par le runtime des workflows ; BLOC_REPRISE est à remplir avec le texte du §5.1.
export const meta = {
name: 'chantier-etape',
description: 'Une étape d’un chantier : seulement les tâches qui restent, trois à la fois',
phases: [{ title: 'Travail' }],
}
// args = { date: '2026-10-05', simultanes: 3, lot: 20,
// taches: [ { id, sortie, consigne, modele, effort } ] } (consigne : le texte complet)
const BLOC_REPRISE = (dossier, n) => `## Écriture et reprise (obligatoire)
...le bloc du §5.1, avec ${dossier} et ${n}...`
const FIN = {
type: 'object',
properties: {
termine: { type: 'boolean' }, lots: { type: 'integer' },
resultats: { type: 'integer' }, remarques: { type: 'string' },
},
required: ['termine', 'lots', 'resultats'],
}
// Au plus `simultanes` agents à la fois ; le premier qui ne va pas au bout arrête les lancements.
async function enFile(taches, simultanes, faire) {
const rendus = []
let suivant = 0
let arret = false
async function ouvrier() {
while (!arret && suivant < taches.length) {
const t = taches[suivant++]
let r
try { r = await faire(t) } catch (e) {
log(`${t.id} : erreur (${e && e.message ? e.message : e})`)
r = null
}
rendus.push({ id: t.id, rendu: r })
if (!r || r.termine !== true) {
arret = true
log(`Arrêt : ${t.id} n'est pas allée au bout. Aucune nouvelle tâche ne démarre.`)
}
}
}
await Promise.all(Array.from({ length: Math.min(simultanes, taches.length) }, ouvrier))
return { rendus, arret, non_lancees: taches.slice(suivant).map(t => t.id) }
}
phase('Travail')
return await enFile(args.taches, args.simultanes || 3, t =>
agent(`Nous sommes le ${args.date}.\n\n${t.consigne}\n\n${BLOC_REPRISE(t.sortie, args.lot || 20)}`, {
label: t.id, phase: 'Travail', schema: FIN, model: t.modele, effort: t.effort,
}))Le retour du script est un confort. Ce qui fait foi après le run, c'est l'inventaire du disque.
5.4 Le crochet qui empêche un sous-agent d'attendre un humain
Ce crochet PreToolUse sur Bash|PowerShell sort en code 2, donc avant les règles de permission, et seulement quand l'appel vient d'un sous-agent (agent_id présent). La session principale garde ses demandes habituelles. L'agent reçoit la raison sur stderr et change de méthode. Il refuse la suppression et le déplacement de fichiers (alias PowerShell compris), et le git destructif : reset --hard, clean, push --force (ou -f, ou --force-with-lease) et add -A / --all / ., où que soient placées les options. Cette liste reprend nos propres règles ask : adapte-la aux tiennes.
// ~/.claude/hooks/refuser-sous-agents.mjs
let entree = ''
process.stdin.setEncoding('utf8')
process.stdin.on('data', morceau => { entree += morceau })
process.stdin.on('end', () => {
let e
try { e = JSON.parse(entree.replace(/^\uFEFF/, '')) } catch { process.exit(0) }
if (!e || !e.agent_id) process.exit(0)
const commande = String((e.tool_input && e.tool_input.command) || '')
// Les alias PowerShell sont ramenés à leur cmdlet avant les règles : on les refuse aussi.
const verbes = e.tool_name === 'PowerShell'
? 'Remove-Item|Move-Item|rm|rmdir|mv|del|erase|ri|rd|mi|move'
: 'rm|rmdir|mv'
const interdit = new RegExp(
`(^|[\\s;|&({])(${verbes})(?=\\s|$)` +
'|(^|[\\s;|&({])git\\b[^;|&\\n]*?\\s(reset\\s+--hard|clean|push\\b[^;|&\\n]*?\\s(--force\\S*|-f)|add\\s+(-A|--all|\\.))(?=\\s|$)',
'i')
if (!interdit.test(commande)) process.exit(0)
process.stderr.write(
"Refusé aux sous-agents : cette commande attendrait un accord humain. Ne supprime, ne déplace " +
"ni ne renomme rien : écris un fichier neuf. Une variable d'environnement se vide par " +
'$env:NOM = $null. Si une suppression est nécessaire, signale-la : la session principale la fera.')
process.exit(2)
})"hooks": {
"PreToolUse": [
{ "matcher": "Bash|PowerShell",
"hooks": [ { "type": "command", "command": "node",
"args": ["<chemin absolu>/refuser-sous-agents.mjs"], "timeout": 10 } ] }
]
}Nous l'avons testé à blanc sur 29 commandes choisies, alias PowerShell et options git déplacées compris : toutes traitées comme prévu. La couverture n'est pas exhaustive pour autant.
Ce qu'il ne fait pas :
- il ne voit que Bash et PowerShell ;
Rename-Itemou[IO.File]::Deletepassent, et restent une affaire de consigne ;- il refuse aussi quelques cas anodins :
git rm, un mot isolé commemoveourm, même dans une chaîne entre guillemets (echo "rm"), oucleandans un message de commit qui ne commence pas par ce mot.
5.5 Avec ou sans Remote Control : pause automatique à la limite, ou échec des agents
- (a) Sans Remote Control pour la session du chantier : la pause à la limite fonctionne. Le run repart seul, y compris la nuit, et peut donc consommer une ou deux fenêtres de plus sans personne devant l'écran.
- (b) Avec Remote Control : les agents échouent à la limite. Grâce aux lots et à l'arrêt au premier échec, on perd au plus un lot par agent en vol, et on relance après la remise à zéro.
inputNeededNotifEnabledenvoie alors une notification sur le téléphone quand une question attend ; elle ne marche qu'avec Remote Control actif.
Notre choix : (b) par défaut, (a) pour un run de nuit assumé.
6. Après un arrêt : inventaire du disque, puis un nouveau run, jamais une reprise
- Ne rien relancer. Vérifier d'abord qu'aucun agent de l'ancien run ne tourne encore : il écrirait dans les mêmes dossiers qu'un agent neuf.
- Faire l'inventaire du disque : faite, entamée, à faire. Un fichier illisible compte comme absent.
- Lancer toujours un nouveau run, avec la liste de ce qui reste. Reprendre l'ancien (
resumeFromRunId) n'apporte rien de plus avec cette méthode, et rejouerait à tort tout ce qui a démarré après un échec (§2.3). Exception : un run mis en pause, et non arrêté. - Arrêter proprement depuis VS Code, c'est arrêter le run entier ; jamais le bouton Stop d'un seul agent, qui compte comme un échec.
7. Repères de coût observés, Max 5x, effort high
À confiance moyenne : ce sont des relevés /usage arrondis, sur une seule soirée.
| Situation | Consommation de la fenêtre de 5 h |
|---|---|
| 10 agents Opus 5.5 en parallèle | 1,36 à 1,5 point par minute, soit ~0,15 par agent |
| 4 agents Fable 5.1 en parallèle | ce qui restait de la fenêtre vidé en ~35 min. Faute de relevé au départ, ~0,7 point par minute et par agent est une borne haute |
| Rapport fenêtre / semaine | 1 point de fenêtre ≈ 0,11 point de semaine sur le relevé le plus large (0,2 sur un relevé plus court, arrondi), soit 5 à 9 fenêtres pleines par semaine |
Pour mesurer avant un gros lancement :
- relever
/usage; - lancer les trois premières tâches ensemble ;
- relever de nouveau, et diviser l'écart par trois.
Une seule tâche coûte trop peu pour l'arrondi de /usage.
8. Ce qui reste à vérifier
- Remote Control comme cause : établi par recoupement, pas par un test contrôlé.
- Un agent en pause à la limite garde-t-il son fil ? Le bloc de reprise couvre les deux cas, mais la réponse change le coût.
- Le crochet en conditions réelles : il n'a été testé qu'à blanc, avec des entrées simulées.
- Les chiffres de coût : une seule soirée, un seul forfait, notre propre taille de
CLAUDE.md.
9. Questions pour ton Claude
- Le crochet en code 2 laisse-t-il passer une voie de blocage : un outil MCP, un
Editsous règleask, unWebFetchà demande de permission ? Faut-il élargir le matcher ? - La file
enFile(desPromise.allsur des « ouvriers ») pose-t-elle problème au runtime des workflows : concurrence, ordre de démarrage, affichage de la progression ? - Lots par l'outil Write ou JSONL par ajout du shell : sur macOS ou Linux, l'ajout par
>>serait-il plus simple et aussi sûr ? - Peut-on détecter de l'intérieur l'approche de la limite (un crochet
StopFailuresurrate_limit, par exemple) pour arrêter proprement entre deux tâches, plutôt que subir la coupure ? - Une définition d'agent avec
omitClaudemd(v2.1.271) pour les agents de collecte : combien de tokens économisés, et quelles règles faut-il alors remettre dans la consigne ? - La granularité de 10 à 20 minutes par tâche est-elle la bonne, compte tenu du ticket d'entrée de chaque agent et du cache de prompt partagé ?