自己進化するフック
3つのフックが、すべての「違う、そういうことじゃない」という修正を、Claudeが次のセッションで読むスキルやルールに変える。プロンプトチューニング不要の自己改善エージェント。
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。
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
設定をやめて、構築を始めよう。
AIオーケストレーション付きSaaSビルダーテンプレート。