Build This Now
Build This Now
リアルなビルド事例アイデアからSaaSへGANループ自己進化するフックトレースからスキルへ販売代理店AIセキュリティエージェント自律型AIスウォームAIメールシーケンスAIが自分自身を掃除する
speedy_devvkoen_salo
Blog/Real Builds/Self-Evolving Hooks

自己進化するフック

3つのフックが、すべての「違う、そういうことじゃない」という修正を、Claudeが次のセッションで読むスキルやルールに変える。プロンプトチューニング不要の自己改善エージェント。

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。

Published Apr 1, 20268 min readReal Builds hub

Claudeはセッションのたびに新鮮な状態で始まる。あなたがem-dashを嫌いなことを覚えていない。完了報告の前にレンダリングするよう指示したことも覚えていない。書き込める場所に書かない限り、何も覚えていない。

フックがこれを解決する。3つのファイルで、Claudeへのすべての修正が自動的にキャプチャ、分析され、プロジェクトに書き戻される。次のセッションでは、エージェントが起動する前からミスは消えている。


フックとは何か?

フックとは、Claude Codeが特定のタイミングで自動実行するスクリプトだ。セッション開始時。サブエージェントが実行される直前。セッション終了時。

.jsファイルを書いてsettings.jsonに登録すると、Claudeが適切なタイミングで呼び出す。フックはstdinでJSONとしてコンテキストを受け取り、処理を行い、終了する。ポーリングなし。管理すべきバックグラウンドプロセスなし。


これを機能させるインサイト

AIラーニングシステムにはすべて同じ問題がある: シグナルはどこから来るのか?

最もクリーンなシグナルはすでにセッションの中にある。「em-dashを削除して」と言えば、それは修正だ。「そう、まさにそれ」と言えば、それは承認だ。あなたがグラウンドトゥルースだ。AIの評価者は不要。循環ループもない。

セッションのトランスクリプト(作業中にClaudeが書くファイル)にはこれがすべて含まれている。あなたが送ったすべてのメッセージ。Claudeが生成したすべてのエージェントと、与えられたプロンプト全文と返された出力全文。読み込まれたすべてのスキルファイル。

3セッション分をキャプチャするとこうなる:

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: []

パターンは明白だ。ドリームワーカーはこれを読んで推論する:「セッションXとYでem-dashへの苦情。両方ともlinkedin-strategistを実行した。セッションZは苦情なしでlinkedin-strategistも実行しなかった。ルールはlinkedin-strategistに書く。」

このロジックをコーディングする必要はない。LLMがやる。それが全体のトリックだ。


最終的なファイルツリー

.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

フック1: エージェント起動前にレッスンを読み込む

エージェントが実行される前に、このスクリプトが起動する。そのエージェントタイプの保存済みレッスンを確認し、あれば<mnemosyne>ブロックに出力する。Claude Codeはここで出力されたものをエージェントのコンテキストの先頭に自動で追加する。

エージェントは前回何が問題だったかをすでに知った状態で起動する。

// .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 ''; }
}

フック2: セッション終了時にキャプチャする

Claudeのセッションが終了すると、このスクリプトが会話全文を読み込み、生のシグナルを抽出する。

正規表現なし。事前分類なし。キャプチャするのは3つだけ: すべての人間のメッセージをそのまま、プロンプトと出力を含む実行したすべてのエージェント、読み込まれたすべてのスキルファイル。以上だ。解釈はドリームワーカーが後で行う。

// .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],
  };
}

ディスク上の1つの観測データはこうなる:

{
  "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"]
}

小さく、読みやすい。セッションごとに1行。解釈は埋め込まれていない。


フック3: ドリームワーカー

2つの条件が満たされたときにバックグラウンドで実行される: 前回の実行から少なくとも4時間経過、かつ少なくとも3つの新しいセッションがキャプチャされている。

Haikuを使ってclaude -pのワンショットプロセスを生成する。ワーカーはプロジェクトへのWriteとEditのアクセス権を持つ。セッション観測データを読み込み、人間のメッセージを自分で分類し、セッション間のパターンを見つけ、適切なファイルにルールを直接書き込む。

// .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."`

ワーカーは直近の20セッションを読み込み、繰り返し修正されていたものを見つけ、レッスンを書く。同じ修正が3セッションあればルールとして書く価値がある。1回の修正はノイズだ。


フックを登録する

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

2つのフック。以上だ。


1週間後に手に入るもの

.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

来週実行されるすべてのエージェントは、先週何が問題だったかをすでに知っている。あなたは何も変更していない。


Posted by @speedy_devv

More in Real Builds

  • AIが自分自身を掃除する
    AIの乱雑さを自動的に掃除する3つの夜間Claude Codeワークフロー: slop-cleanerがデッドコードを削除し、/healが壊れたブランチを修復し、/driftがパターンドリフトを捉えます。
  • GANループ
    1つのエージェントが生成し、もう1つが徹底的に批評し、スコアが改善しなくなるまでループする。エージェント定義とルーブリックテンプレートを含むGANループの実装。
  • AIメールシーケンス
    Claude Codeの1コマンドで6シーケンス17本のライフサイクルメールを生成し、Inngestの行動トリガーを配線してデプロイ可能な分岐型メールファネルを構築します。
  • AIセキュリティエージェント
    2つのClaude Codeコマンドで8つのセキュリティサブエージェントを起動。フェーズ1はSaaSロジックのRLSの欠陥と認証バグをスキャンし、フェーズ2は実際の攻撃を試みて本物の脆弱性を確認します。
  • 自律型AIスウォーム
    自律型Claude Codeスウォーム: 30分トリガー、オーケストレーター、ワークツリー内の専門サブエージェント、そして夜間に安全に機能をリリースする5つのゲート。
  • 販売代理店
    4つのクロード・コード・エージェントは、スケジュール通りに動作し、SEO記事を書き、PostHogを読み、カルーセルを構築し、Redditをスカウトする。定義をコピーしてプラグインする。

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。

On this page

フックとは何か?
これを機能させるインサイト
最終的なファイルツリー
フック1: エージェント起動前にレッスンを読み込む
フック2: セッション終了時にキャプチャする
フック3: ドリームワーカー
フックを登録する
1週間後に手に入るもの

設定をやめて、構築を始めよう。

AIオーケストレーション付きSaaSビルダーテンプレート。