# Longs travaux d'agents Claude Code : pourquoi on perd du travail à la coupure, et comment ne plus rien perdre

*Retour d'expérience du 5 octobre 2026. Forfait Max 5x, extension VS Code sous Windows, Claude Code
2.1.286 au moment de l'incident, documentation officielle relue le jour même (CLI 2.1.289).*

**À 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 `/usage` fait 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 `autoContinueAtUsageLimit` est 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`, ou `claude remote-control`) ;
  - une session en arrière-plan ;
  - `claude -p` et l'Agent SDK ;
  - une remise à zéro à plus de 24 h, typiquement une limite hebdomadaire ;
  - une troisième limite d'affilée.

**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()` et `new Date()` lèvent une erreur. Il ne
  peut donc pas savoir ce qui est déjà fait : il faut le lui dire par `args`. [doc, « Behavior and
  limits »]
- **`agent()` rend `null` après un échec définitif**, et peut aussi lever une exception, par exemple
  sur un schéma de sortie raté cinq fois. [doc]
- **`agent()` accepte `model` et `effort` par 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 `ask` de `settings.json` priment sur le mode auto.** Un agent qui voulait vider une
  variable d'environnement par `Remove-Item Env:X` a 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** : `del` et `ri` déclenchent
  une règle `Remove-Item`. [doc, `permissions`]
- **Les crochets des fichiers de réglages s'appliquent aussi aux outils des sous-agents.** Leur
  entrée porte `agent_id` et `agent_type`, ce qui permet de viser les seuls sous-agents. [doc,
  `hooks`]
- **Un crochet `PreToolUse` qui sort en code 2 bloque l'appel avant l'évaluation des règles de
  permission.** Une décision JSON `allow` ou `ask`, elle, ne contourne pas une règle `ask`. [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

1. **La coupure : sans doute Remote Control.** [hypothèse forte] La session avait été ouverte avec
   `remoteControlAtStartup: true` ; son journal contient des enregistrements `bridge-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.
2. **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.
3. **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.
4. **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.

1. **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.
2. **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.
3. **Une marque de fin.** L'agent qui a fini écrit `FIN.json`. Sans `FIN.json` lisible, la tâche est
   inachevée.
4. **L'agent sait reprendre.** Sa consigne lui dit de lire ses lots existants et de continuer après
   (§5.1).
5. **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.
6. **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.
7. **Trois agents à la fois au plus**, sauf décision explicite, réglés à chaque run.
8. **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.
9. **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

```json
{
  "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-*.json` ni `FIN.json` : à faire ;
- des lots sans `FIN.json` : entamée ;
- `FIN.json` lisible : 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.

```js
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**.

```js
// ~/.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)
})
```

```json
"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-Item` ou `[IO.File]::Delete` passent, et restent une affaire de consigne ;
- il refuse aussi quelques cas anodins : `git rm`, un mot isolé comme `move` ou `rm`, même dans
  une chaîne entre guillemets (`echo "rm"`), ou `clean` dans 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.
  `inputNeededNotifEnabled` envoie 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

1. **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.
2. **Faire l'inventaire du disque** : faite, entamée, à faire. Un fichier illisible compte comme
   absent.
3. **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é.
4. **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 :
1. relever `/usage` ;
2. lancer les trois premières tâches ensemble ;
3. 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

1. Le crochet en code 2 laisse-t-il passer une voie de blocage : un outil MCP, un `Edit` sous
   règle `ask`, un `WebFetch` à demande de permission ? Faut-il élargir le matcher ?
2. La file `enFile` (des `Promise.all` sur des « ouvriers ») pose-t-elle problème au runtime des
   workflows : concurrence, ordre de démarrage, affichage de la progression ?
3. 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 ?
4. Peut-on détecter de l'intérieur l'approche de la limite (un crochet `StopFailure` sur
   `rate_limit`, par exemple) pour arrêter proprement entre deux tâches, plutôt que subir la
   coupure ?
5. 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 ?
6. 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é ?
