Build This Now
Build This Now
Vrais BuildsDe l'idée au SaaSBoucle GANHooks Auto-ÉvolutifsDe la trace au skillAgents de distributionAgents de sécurité IAEssaim IA autonomeSéquences d'emails IAL'IA se nettoie elle-même
speedy_devvkoen_salo
Blog/Real Builds/Self-Evolving Hooks

Hooks Auto-Évolutifs

Trois hooks transforment chaque correction 'non, pas comme ça' en skill ou règle que Claude lit à la prochaine session. Des agents qui s'améliorent seuls, sans tuning de prompt.

Arrêtez de configurer. Commencez à construire.

Templates SaaS avec orchestration IA.

Published Apr 1, 20268 min readReal Builds hub

Claude repart de zéro à chaque session. Il ne se souvient pas que tu détestes les tirets cadratins. Il ne se souvient pas que tu lui as dit de rendre avant d'annoncer que c'est fini. Il ne se souvient de rien, sauf si tu l'écris quelque part où il peut le lire.

Les hooks règlent ça. Trois fichiers, et chaque correction que tu donnes à Claude est capturée, analysée, et réécrite automatiquement dans ton projet. À la session suivante, l'erreur a disparu avant même que l'agent commence.


C'est quoi un hook ?

Un hook est un script que Claude Code lance automatiquement à des moments précis. Quand une session commence. Quand un sous-agent est sur le point de tourner. Quand la session se termine.

Tu écris un fichier .js, tu l'enregistres dans settings.json, et Claude l'appelle au bon moment. Le hook reçoit le contexte sous forme de JSON sur stdin, fait ce que tu veux, et sort. Pas de polling. Pas de processus en arrière-plan à gérer.


L'intuition qui rend ça possible

Tous les systèmes d'apprentissage IA ont le même problème : d'où vient le signal ?

Le signal le plus propre est déjà dans ta session. Quand tu dis "non, supprime le tiret cadratin", c'est une correction. Quand tu dis "oui exactement", c'est une approbation. Tu es la vérité terrain. Pas besoin d'évaluateur IA. Pas de boucle circulaire.

La transcription de session (le fichier que Claude écrit pendant que tu travailles) contient tout ça. Chaque message que tu as envoyé. Chaque agent que Claude a lancé, avec le prompt complet qui lui a été donné et la sortie complète qu'il a retournée. Chaque fichier de skill qui a été lu.

Voici à quoi ressemblent trois sessions une fois capturées :

session X:
  human_messages: ["write a LinkedIn post", "no em-dashes please", "yes that's better"]
  agents_run: [{ type: "linkedin-strategist", output: "post with em-dash — great hook" }]
  skills_read: ["linkedin-strategist"]

session Y:
  human_messages: ["write another post", "still has em-dashes wtf", "good"]
  agents_run: [{ type: "linkedin-strategist", output: "The future is here — changing everything" }]
  skills_read: ["linkedin-strategist"]

session Z:
  human_messages: ["write a carousel", "looks good"]
  agents_run: [{ type: "carousel-designer" }]
  skills_read: []

Le pattern est évident. Le dream worker lit ça et raisonne : "plainte sur les tirets cadratins dans les sessions X et Y. Les deux ont lancé linkedin-strategist. La session Z n'avait aucune plainte et n'a pas lancé linkedin-strategist. La règle va dans linkedin-strategist."

Tu ne codes pas cette logique. Un LLM le fait. C'est tout le truc.


Arborescence finale des fichiers

.claude/
  hooks/
    subagent-start.js    <- agents wake up with lessons already loaded
    on-stop.js           <- captures the session raw, no pre-classification
    dream/
      dream.js           <- finds patterns, writes rules
  learning/
    sessions/
      2026-04-08.jsonl   <- one observation per session
    global.md            <- lessons that apply to everything
    agents/
      linkedin-strategist.md  <- lessons for one specific agent
  settings.json

Hook 1 — Charger les leçons avant que l'agent commence

Avant qu'un agent tourne, ce script se déclenche. Il vérifie s'il y a des leçons sauvegardées pour ce type d'agent. S'il y en a, il les affiche dans un bloc <mnemosyne>. Claude Code préfixe automatiquement tout ce qui est affiché ici au contexte de l'agent.

L'agent se réveille en sachant déjà ce qui a merdé la dernière fois.

// .claude/hooks/subagent-start.js
'use strict';
const fs   = require('fs');
const path = require('path');

const root    = path.resolve(__dirname, '..', '..', '..');
const coreDir = path.join(root, '.claude', 'core');

let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
  try {
    const event     = JSON.parse(raw);
    const agentType = (event.agent_type || '').replace(/^[^:]+:/, '').trim().toLowerCase();
    const parts     = [];

    // Global lessons — apply to every agent
    const global = readFile(path.join(coreDir, 'learning', 'global.md'));
    if (global) parts.push(`### Global Learnings\n\n${global}`);

    // Agent-specific lessons
    if (agentType) {
      const learned = readFile(path.join(coreDir, 'learning', 'agents', `${agentType}.md`));
      if (learned) parts.push(`### Learnings for ${agentType}\n\n${learned}`);
    }

    if (parts.length === 0) { process.exit(0); return; }

    const attr = agentType ? ` agent="${agentType}"` : '';
    process.stdout.write(`<mnemosyne${attr}>\n\n${parts.join('\n\n')}\n\n</mnemosyne>\n`);
  } catch {}
  process.exit(0);
});

function readFile(p) {
  try { return fs.readFileSync(p, 'utf8').trim(); } catch { return ''; }
}

Hook 2 — Capturer la session quand elle se termine

Quand ta session Claude se ferme, ce script lit la conversation complète et en extrait le signal brut.

Pas de regex. Pas de pré-classification. Il capture trois choses : chaque message humain mot pour mot, chaque agent qui a tourné avec son prompt et sa sortie, chaque fichier de skill qui a été lu. C'est tout. Le dream worker fait l'interprétation plus tard.

// .claude/hooks/on-stop.js
'use strict';
const fs     = require('fs');
const path   = require('path');
const crypto = require('crypto');
const { spawn } = require('child_process');

const root    = path.resolve(__dirname, '..', '..', '..');
const coreDir = path.join(root, '.claude', 'core');

const COOLDOWN_MS  = 4 * 3_600_000;
const MIN_SESSIONS = 3;

let raw = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
  try {
    const event = JSON.parse(raw);
    const { session_id, transcript_path } = event;
    if (!transcript_path || !fs.existsSync(transcript_path)) { process.exit(0); return; }
    const obs = parseSession(session_id || 'unknown', transcript_path);
    writeObservation(obs);
    if (shouldDream()) spawnDream();
  } catch {}
  process.exit(0);
});

function parseSession(sessionId, transcriptPath) {
  const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n').filter(Boolean);
  const humanMessages = [];
  const agentsRun     = [];
  const skillsRead    = new Set();
  let pendingAgent    = null;

  for (const line of lines) {
    let e; try { e = JSON.parse(line); } catch { continue; }
    const role    = e.message?.role;
    const content = e.message?.content;
    if (!Array.isArray(content)) continue;

    for (const block of content) {
      // Every human message, verbatim
      if (role === 'user' && block.type === 'text') {
        const text = (block.text || '').trim();
        if (text.length > 2) humanMessages.push(text.slice(0, 300));
      }

      // Agent output arrives as a tool_result
      if (role === 'user' && block.type === 'tool_result' && pendingAgent) {
        const parts = Array.isArray(block.content)
          ? block.content
          : [{ type: 'text', text: String(block.content || '') }];
        const meta = parts.find(p => p.type === 'text' && p.text?.includes('agentId:'));
        if (meta) {
          const output = parts
            .filter(p => p !== meta && p.type === 'text' && p.text)
            .map(p => p.text).join('\n').trim();
          agentsRun.push({
            type:           pendingAgent.type,
            prompt_preview: pendingAgent.prompt,
            output_preview: output.slice(0, 400).replace(/\s+/g, ' '),
          });
          pendingAgent = null;
        }
      }

      // Track what was spawned and what was read
      if (role === 'assistant' && block.type === 'tool_use') {
        if (block.name === 'Agent') {
          const t = (block.input?.subagent_type || 'unknown')
            .replace(/^[^:]+:/, '').toLowerCase();
          pendingAgent = { type: t, prompt: (block.input?.prompt || '').slice(0, 150) };
        }
        if (block.name === 'Read') {
          const m = (block.input?.file_path || '').match(/skills\/([^/]+)\/SKILL\.md$/i);
          if (m) skillsRead.add(m[1]);
        }
      }
    }
  }

  return {
    id:              `sess-${Date.now()}-${crypto.randomBytes(2).toString('hex')}`,
    ts:              new Date().toISOString(),
    session_id:      sessionId,
    transcript_path: transcriptPath,
    human_messages:  humanMessages,
    agents_run:      agentsRun,
    skills_read:     [...skillsRead],
  };
}

Voici à quoi ressemble une observation sur le disque :

{
  "ts": "2026-04-08T14:32:11.000Z",
  "session_id": "a7b3c2d",
  "human_messages": [
    "Write a LinkedIn post about AI agents",
    "no don't use em-dashes, remove them",
    "yes exactly, that is what I wanted"
  ],
  "agents_run": [{
    "type": "linkedin-strategist",
    "prompt_preview": "Write a LinkedIn post about AI agents building tools",
    "output_preview": "Here is the post. It uses an em-dash to make it punchy..."
  }],
  "skills_read": ["linkedin-strategist"]
}

Compact. Lisible. Une ligne par session. Pas d'interprétation intégrée.


Hook 3 — Le dream worker

Celui-ci tourne en arrière-plan quand deux conditions sont remplies : au moins 4 heures depuis le dernier run, et au moins 3 nouvelles sessions capturées.

Il lance un processus claude -p à usage unique avec Haiku. Le worker a accès en écriture et édition à ton projet. Il lit les observations de session, classe lui-même les messages humains, trouve des patterns à travers les sessions, et écrit les règles directement dans les bons fichiers.

// .claude/hooks/dream/dream.js (the prompt sent to Haiku)
`You analyze recent sessions and write one-line rules to prevent repeated mistakes.

★ = new since last dream. These are fresh signal.

## Sessions

★ 2026-04-08T14:32 | agents:[linkedin-strategist] | skills:[linkedin-strategist]
  Human messages:
    1. "Write a LinkedIn post about AI agents"
    2. "no don't use em-dashes, remove them"
    3. "yes exactly, that is what I wanted"
  linkedin-strategist output: "Here is the post. It uses an em-dash to make it punchy..."

★ 2026-04-07T10:15 | agents:[linkedin-strategist] | skills:[linkedin-strategist]
  Human messages:
    1. "write another post"
    2. "still has em-dashes wtf"
    3. "good"
  linkedin-strategist output: "The future is here — changing everything about how..."

## Where to write

- .claude/learning/agents/{type}.md   for one specific agent
- .claude/learning/global.md          for every agent
- .claude/skills/{name}/SKILL.md      fix the skill that caused the mistake

## Rules

- 1 session = noise. Same correction in 2+ sessions = write it.
- One-line rules only. Specific, not vague.
- Read the target file first. Do not duplicate existing rules.
- Max 5 new rules per run.

Good: "Never use em-dashes. Use commas or short sentences instead."
Bad: "Be more careful with formatting."`

Le worker lit tes 20 dernières sessions, repère ce que tu corrigeais sans cesse, et écrit la leçon. Trois sessions avec la même correction, c'est une règle qui mérite d'être écrite. Une seule correction, c'est du bruit.


Enregistrer les hooks

{
  "hooks": {
    "SubagentStart": [{
      "type": "command",
      "command": "node .claude/hooks/subagent-start.js"
    }],
    "Stop": [{
      "type": "command",
      "command": "node .claude/hooks/on-stop.js",
      "async": true
    }]
  }
}

Deux hooks. C'est tout.


Ce que tu obtiens après une semaine

.claude/
  learning/
    global.md
      Never use em-dashes. Use commas or short sentences instead.
      <!-- dream 2026-04-08 -->

    agents/
      linkedin-strategist.md
        Always write in first person when the topic is personal experience.
        <!-- dream 2026-04-09 -->

  skills/
    linkedin-strategist/
      SKILL.md   <- 2 new rules added from repeated corrections

Chaque agent qui tourne la semaine prochaine sait déjà ce qui a cassé la semaine dernière. Tu n'as rien changé.


Posté par @speedy_devv

More in Real Builds

  • L'IA se nettoie elle-même
    Trois workflows Claude Code overnight qui nettoient le bazar de l'IA : slop-cleaner supprime le code mort, /heal répare les branches cassées, /drift détecte la dérive des patterns.
  • Boucle GAN
    Un agent génère, l'autre le démonte, ils bouclent jusqu'à ce que le score cesse de s'améliorer. Implémentation de la boucle GAN avec définitions d'agents et modèles de rubrique.
  • Séquences d'emails IA
    Une commande Claude Code construit 17 emails de cycle de vie sur 6 séquences, câble les déclencheurs comportementaux Inngest, et livre un funnel d'emails à embranchements prêt à déployer.
  • Agents de sécurité IA
    Deux commandes Claude Code lancent huit sous-agents de sécurité : la phase 1 scanne la logique SaaS pour détecter les failles RLS et les bugs d'auth, la phase 2 pénètre pour confirmer les vraies exploits.
  • Essaim IA autonome
    Un essaim Claude Code autonome : un déclencheur de 30 min, un orchestrateur, des sous-agents spécialisés dans des worktrees, et cinq portes qui livrent des fonctionnalités en toute sécurité pendant la nuit.
  • Agents de distribution
    Quatre agents Claude Code qui s'exécutent selon un calendrier, écrivent des posts SEO, lisent PostHog, construisent des carrousels et font du repérage sur Reddit. Copie les définitions et branche-les.

Arrêtez de configurer. Commencez à construire.

Templates SaaS avec orchestration IA.

On this page

C'est quoi un hook ?
L'intuition qui rend ça possible
Arborescence finale des fichiers
Hook 1 — Charger les leçons avant que l'agent commence
Hook 2 — Capturer la session quand elle se termine
Hook 3 — Le dream worker
Enregistrer les hooks
Ce que tu obtiens après une semaine

Arrêtez de configurer. Commencez à construire.

Templates SaaS avec orchestration IA.