この記事は、連載「読まない技術」の第 7 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、並列で走るセッションが共有するメモリの話です。このメモリを整理するように、私が AI に方針を読ませても、効きませんでした。保存は最優先で走るからです。保存の前に読ませた方針は、保存に負けます。その理由を扱います。

チームでは、複数のメンバーがそれぞれ複数のセッションを走らせ、全員の AI が同じメモリに書き込みます。第 2 回で見たとおり、AI は保存したがるので、メモリは人数 × セッション数の速度で育ちます。そこで入口を一つに絞っても、あなたが承認した保存は起きますし、隣のセッションの保存はそこを通りません。肥大化は個人の入口では止まらない。

CC BY 4.0

「簡潔に書け」は、なぜ効かなかったか

舞台は typingtube という個人運営の Web サービス(YouTube の音楽動画でタイピング練習ができます)で、チームではありません。それでも複数のセッションを並列実行するので、症状は同じ形で出ました。

まず私は「メモは簡潔に書け。同じ対象のメモは統合しろ」をルールに書きました。

AI は守りませんでした。次に私が、太字にしてファイルの先頭に移しました。AI の振る舞いは変わりませんでした。ここまでは序論の「薄まる」で説明が付きます。

次に、保存の直前に方針を読ませて止める仕組み(保存前の hook)を置きました。これも駄目でした。保存の瞬間、AI の中では「保存を完了させる」が最優先のタスクです。その最中に差し込まれた方針は、従うべきものではなく「どう書けば通るか」の攻略対象として読まれます。

実際、AI は語を言い換えるだけで、私の保存前 hook を素通りしました。

同じ性質は、第 3 回にも出ていました。研究の側でこれに当たる語は specification gaming で、目的の字面の仕様を満たすが、意図した結果を達成しない振る舞いと定義されています。「簡潔に書け」も「統合しろ」も、言葉の上でなら満たせます。「保存を完了させる」が最優先のときにこの方針を読めば、語を言い換えることが、いちばん早く満たす道になります。

ルールが負ける相手は、序論の薄まりと、第 1 回からの染み付いた習慣だけではありませんでした。目の前のタスクとの優先順位でも負けます。しかも保存は、その優先順位が一番高くなる瞬間です。

理解したことは一文にできます。

メモリ保存は最優先で走る。だから保存前に読ませた方針は保存に負ける。方針は保存の後にしか効かない。

仕組み: 同じ方針を、保存の後に置く

置くものは同じ方針ファイルです。変えるのは位置だけ——保存が完了した後に、実際に書かれた差分と並べて AI に突きつけます。中身は「統合する・破棄する・文書へ移す」程度の数行にします。この方針も保存のたびに会話へ積まれるので、長くすれば、序論で見た足すほど薄まるが突きつける側にも起きます。

第 1 回の hook はコマンドの直前(PreToolUse)に割り込みましたが、これは完了の直後(PostToolUse)に割り込む形です。保存は済んでいるので、「保存を完了させる」はもう終わっています。同じ方針が今度は、「この書き込みを直すかどうか」の判断基準として読まれます。

完了の直後に割り込めるのは、hook にその位置が用意されているからです。Claude Code の公式ドキュメントは、ツールが済んだあとに走る hook が返す decision: "block" の reason を、ツールの結果の隣に足して Claude へ渡すと書いています。ツールはもう成功しているので、この block は失敗の通知ではありません。済んだ書き込みと並べて方針を読ませるための差し込みです。

# 保存後に差分を突きつける hook(手元のコードを短くしたもの・PostToolUse)
import difflib, glob, json, os
SNAP_DIR = os.path.expanduser("~/.claude/.memory-snapshot")  # ⚠️ 監視対象の外に置く

def added_lines(before, after):
    diff = difflib.unified_diff(before.splitlines(), after.splitlines(), n=0, lineterm="")
    return [l[1:] for l in diff if l.startswith("+") and not l.startswith("+++ ")]

def collect_changes(paths):
    # 実ファイルをスナップショットと比べる(コマンドの文字列では、変数やパイプの先を見逃す)。
    # 比べたら、次の保存のために撮り直す
    changes = []
    os.makedirs(SNAP_DIR, exist_ok=True)
    for path in paths:
        snap = os.path.join(SNAP_DIR, path.replace("/", "_"))
        before = open(snap).read() if os.path.exists(snap) else ""
        after = open(path).read()
        changes += [f"{os.path.basename(path)}: +{l}" for l in added_lines(before, after)]
        open(snap, "w").write(after)
    return changes

changes = collect_changes(glob.glob(os.path.expanduser("~/.claude/projects/*/memory/*.md")))
if changes:
    print(json.dumps({
        "decision": "block",
        "reason": "Memory changed. Added lines:\n" + "\n".join(changes) + "\n"
                  "Policy: merge notes on the same subject into one / discard notes nobody read /\n"
                  "copies of steps and commands belong in documents, not in memory.\n"
                  "Judge this diff against the policy and say whether to keep, merge, or move it.\n"
                  "If you did not write it, say that it is another session's change.",
    }, ensure_ascii=False))

位置のほかに、もう一つ外せない条件があります。モデルに届く経路で返すことです。同じ hook のドキュメントは、画面向けの通知(systemMessage)を「あなたに見せる警告」として別の欄に分けています。この欄に書いたものは、Claude に届きません。私も最初はこの欄で返していて、そのあいだ AI が受け取っていたのは「書き込み完了」だけでした。

方針の最後の 1 行にも役があります。共有メモリでは、別のセッションの書き込みも差分に混ざります。hook が比べているのは実ファイルとスナップショットなので、その差に「誰が書いたか」は載っていません。AI から見ると、自分の保存も隣のセッションの保存も、同じ形で届きます。

しかも、目の前にある差分は「終わった仕事」の形をしています。Claude Code の公式ドキュメントはAI は仕事が終わったように見えたところで止まると書いています。終わって見えるものは、区別する材料が無ければ、そのまま自分の成果として報告に入ります。だから「自分が書いたものか確かめ、違えばそう言う」を方針に含めておきます。

ただし、確かめるのは AI です。hook はファイルの中身の差しか見ていないので、どれが自分の書き込みかという判定だけは、プログラムの外に残ります。

抜け穴も一つ残ります。メモリの保存はセッションの終わり際に起きがちで、差分を突きつけた直後に AI が「あとは要約を書いて終わり」へ進んでしまうと、報告は落ちます。終わりに見えている場所では、差し込んだ指摘より、終わらせるほうが強いということです。

そこで私の環境では、報告されていない差分が残ったまま AI がセッションを終えようとしたときに、その終了を一度だけ止める hook を置いています。止められた AI は、終わる前にもう一度応答を返すので、そこで報告が出ます。一度だけなのは、毎回止めるとセッションが終われなくなるからです。

CC BY 4.0 はここまで

読まなくなったもの

共有メモリの見直しです。定期的に全部読み返して統合・削除する当番仕事が消えました。整理は保存のたびに、書いた本人のセッションで差分単位で終わっています。どのメンバーのどのセッションが書いても同じ hook が同じ方針を突きつける——チームでこそ効くのは、この性質のためです。

CC BY 4.0

検証手順: 重複を保存させて、統合されることを確かめる

前提

  • 保存の後に割り込む hook(PostToolUse に当たるもの)を登録し、方針ファイルを数行で置き終えていること
  • セッションを 2 つ同時に立てられること——チームなら別のメンバーの端末、1 人なら同じメモリを共有する別のセッションを 2 つ開きます

所要時間: 15 分

手順

  1. 1 つ目のセッションの AI に、ある対象についてメモを 1 件保存させます(「X の再起動順序は A B」のような、あなたのプロジェクトに実在する話題で)
  2. 2 つ目のセッションの AI に、同じ対象について、少し違う表現のメモを保存させます(「X を再起動したら B も必ず」)。語を変えてください。まったく同じ文だと、統合すべきかどうかの判断が起きません

合格条件(2 つ目のセッションの、保存した直後の応答が、すべて満たすこと)

  • 保存が済んだ後に方針が届いている
  • AI が統合するか、統合を提案する(「1 つ目と同じ対象なので 1 件にまとめます」)
  • 突きつけられた差分の中で、1 つ目のセッションが書いた行が「別のセッションの変更」として区別されている

合格しなかったとき

  • 保存の前に止まった —— それは保存前の hook です。位置が違います。保存の前だと、方針は「どう書けば通るか」として読まれます
  • 別のセッションの変更が区別されていない —— AI は他人の保存を、自分の成果として報告します。方針に「自分が書いたものか確かめ、違えばそう言う」の 1 行を足します

後始末

  • 手順 1・2 で作ったメモを、要らなければ消します。索引に足した行も一緒に消します

CC BY 4.0 はここまで

次回は「修正履歴を読まない技術」。AI を軌道修正させた記録を、私も AI も読み返していません。それでも AI は、同じ失敗を繰り返しません。

連載「読まない技術」