はじめに
Claude Codeを毎日使っていると、こんな不満が積み重なることがあります。
受け答えがおかしくなる
すぐに忘れてしまう
勝手な決めつけ
さっきできたことが次はできなくなる
いわゆるおま環の可能性もあります
やってみて、だめならまた考えます
今回は「勝手に決めつける」を実際に指摘したところから始まり、原因を探って対策を組んだ話です。
最終的にサブエージェントを6つに分けて役割を固定し、セッション開始時にフックで運用ルールを強制的に思い出させる構成にしました。
フックをつけたことでセッションの起動が少し遅くなりましたが
変なことされるよりいいか
実際に起きた「決めつけ」
同じセッション内で、自分の職業と居住地を2つとも間違えられました。
どちらも一次情報として保存していたメモリファイルには正しい値が書いてあったのに、それを確認せず、別案件のテーマ地域や大まかな職種イメージから補完してしまっていました。
過去のセッションを横断検索してみると、同じ種類のミスが同日中に複数回起きていたこともわかりました。
指摘されると「失礼しました、決めつけて書いてしまいました」と謝ってその場では直すものの、次の機会にまた同じことをするというサイクルです。
口調(タメ口や常体)が崩れる件についても同じパターンが繰り返されていて、メモリに「何回言ってもダメ」という記録が何件も積み上がっていました。
なぜ直らないのか
メモリの仕組み自体はすでにあります。
セッションを開始すると、MEMORY.mdという索引ファイルが自動的にコンテキストへ読み込まれます。
ただしこの索引は1ファイルにつき1行程度の要約で、本文の詳細までは含まれていません。
たとえば「職業」の詳細は索引には載っておらず、職務経歴に関する本体ファイルを実際に開かないとわかりません。
索引だけを見て「なんとなくこんな感じだろう」と補完すると、今回のような間違いが起きます。
もう一つの問題は、CLAUDE.mdやメモリに指示を書いても、生成する瞬間に確実にそれを踏まえる保証がないことです。
「口調は敬語で」というルールは何ヶ月も前から書いてあるのに、
指摘→謝罪→再発
が延々と続いていました。
ルールが存在することと、実行時に守れているかは別問題だと認めるところから対策を考え直しました。
サブエージェント6個体制にする
そこで、1つのエージェントがすべてを判断する構成をやめて、役割ごとにサブエージェントを分けることにしました。
Claude Codeではプロジェクト共通の~/.claude/agents/にMarkdownでサブエージェントを定義できます。
今回作ったのは次の6つです。
- memory-lookup:事実に関わる主張を書く前に、索引ではなくメモリ本体を読んで確認する専門役(読み取り専用)
- memory-writer:新しい学びやプロジェクト状況をメモリファイルとして保存する役
- planner:非自明な実装や企画の方針を設計する役(読み取り専用)
- coder:実装や編集、テスト実行を担当する役
- watchdog:他のエージェントの成果物を事後レビューし、問題があれば差し戻す役
- session-boot:タスクに関連するメモリ本体やCLAUDE.mdを能動的に読み込み、要点をまとめて渡す役
memory-lookupの定義ファイルはこんな内容にしました。
---
name: memory-lookup
description: 事実に関わる主張を書く前に、メモリ索引だけでなく該当ファイル本体を読んで正確な一次情報を返す。読み取り専用。
tools: Read, Grep, Glob
---
あなたはメモリ検索専門のサブエージェントです。呼び出し元から「確認したい事実」を受け取り、以下の手順で調べて正確な情報のみを返します。
1. MEMORY.mdの索引から関連しそうなファイルを特定する。
2. 索引の一行要約だけで済ませず、該当ファイル本体を実際に読む。
3. メモリに記載がない、または古い可能性がある場合は「メモリに記載なし」と明記する。推測で埋めない。
ポイントは、tools指定でReadとGrep、Globだけに絞っていることです。
このエージェントには編集権限を持たせていないので、調べる以外の動作をしようがない構成になっています。
watchdogにだけ差し戻し権限を持たせる
6つの中でwatchdogだけは、他のサブエージェントを直接呼び出す権限(Agent tool)を持たせています。
コードやメモリの成果物をレビューして、CLAUDE.mdの指示違反や事実確認不足を見つけたら、該当のサブエージェントに直接差し戻して修正させます。
ただしこれで決めつけや口調崩れがゼロになるとは考えていません。
watchdog自身も同じ言語モデルなので、見落とす可能性は残ります。
リアルタイムで他のエージェントを監視することもできないので、あくまで作業が終わった後にレビューする役割です。
SessionStartフックで運用ルールを強制注入する
サブエージェントを作っただけでは、そもそも「必要な場面でmemory-lookupを呼ぶ」という判断自体をメイン側がサボる可能性が残ります。
これまでの再発パターンを見ていると、まさにこの「判断そのものが飛ぶ」ケースが多かったので、Claude CodeのSessionStartフックを使うことにしました。
最初はMEMORY.mdの中身をそのままフックでもう一度出力しようとしたのですが、これはやめました。
MEMORY.mdはセッション開始時にすでに自動でコンテキストに読み込まれているため、同じ内容を二重に注入しても情報量は増えません。
フックで意味があるのは、索引の中身ではなく「事実の主張前に必ず確認する」という運用ルールそのものを、会話が長くなって圧縮されても薄れない形で毎回突っ込むことでした。
作ったフックスクリプトです。
#!/usr/bin/env python3
import json
REMINDER = (
"[起動時強制リマインダー] MEMORY.mdの索引は既に読み込み済みだが、索引は要約であり詳細ではない。"
"本人の職業・居住地・経歴・プロジェクトの状態など事実に関わる主張を書く前には、"
"索引の記載だけで判断せず、必ずmemory-lookupサブエージェント(または該当メモリファイル本体)を確認してから書くこと。"
"確認せずに別案件の文脈や大まかな印象から属性を補完してはならない。"
"非自明な実装・企画はplannerサブエージェントに設計させ、コーディングはcoderサブエージェントに任せ、"
"成果物はwatchdogサブエージェントにレビューさせ、指摘があれば差し戻して修正すること。"
"新しい学び・フィードバック・プロジェクト状況はmemory-writerサブエージェントに保存させること。"
)
def main():
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": REMINDER,
}
}))
if __name__ == "__main__":
main()
~/.claude/settings.jsonにはこのように追記しています。
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python3 /Users/yuukiyamashita/.claude/hooks/session_boot_reminder.py",
"timeout": 5
}
]
}
]
}
SessionStartフックは標準出力をJSONで返すと、その内容がそのままセッション開始時のコンテキストに追加されます。
hookSpecificOutput.additionalContextにリマインダー文を入れているだけの、シンプルな仕組みです。
動作確認は、echo '{}' | python3 session_boot_reminder.pyでスクリプト単体を実行し、意図したJSONが出力されることを確認しました。
その上でjqコマンドでsettings.jsonの構文とネスト構造をチェックしています。
構成図
今回組んだ構成をざっくり図にすると、SessionStartフックがメインエージェントにルールを注入し、メインが記憶系3役(memory-lookup、session-boot、memory-writer)と実行系(planner→coder)、監督役(watchdog)に振り分ける形になります。
まだ検証中
この構成を組んですぐのタイミングで、長めのセッションを1本通してみたところ、口調の崩れは最後まで起きませんでした。
ただし過去にも一時的にうまくいった後で再発した記録が残っているので、これで直ったと結論づけるのは早いと思っています。
しばらく実運用しながら、決めつけや物忘れが実際に減るのか、地道に見ていくつもりです。
最後まで読んでいただきありがとうございました。
