この記事は、連載「読まない技術」の第 6 回です。各回はファイルやスクリプトを一つ置けば完結します。連載の全体像と各回の一覧は序論にあります。

今回は、指示ファイルに書くルールの話です。守ってほしいことを書くのをやめて、代わりに何を置くかを扱います。

まず一つだけ、確かめてみてください。あなたの指示ファイルに書いてある禁止事項を 1 つ選びます。開発のときに AI に許さないと決めたルールのことです。それが先月何回破られたか、答えられますか。

答えられないのが普通です。AI に破られても何も起きないからです。ルールが効くには、AI に読まれる → 覚えられている → 従われる、の三つが全部成立する必要があり、序論で見たとおり、どれもルールが増えるほど怪しくなります。

一方で、あなたのプロジェクトには誰も読んでいないのに毎回守られているルールが既に大量にあります。lint、パターンチェック、CI。あれが守られるのは読まれているからではなく、破ると落ちるからです。

この回でやることは、その既知の思想を AI 向けのルールに全部適用するだけです。チームに持ち込むときに一番通りやすい回でもあります。

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

ルールは読まれたときだけ効く。自動テストや hook は読まれなくても落ちる。

これは気の持ちようの話ではなく、届き方の違いです。Claude Code の公式ドキュメントは、指示ファイルとメモリの中身はシステムプロンプトではなく、その後のユーザーメッセージとして届くと書いています。文脈として扱われ、強制される設定としては扱われない——動作を止めたいなら hook を使え、とまで書いてあります。その hook の側には「LLM がそれを実行すると選ぶことに頼るのではなく、その動作が必ず起きる」とあります。同じ 1 行でも、置いた場所で通り道が変わります。

flowchart TB
  Y["あなたが書いた 1 行"] --> Q{"どこに置いたか"}
  Q -- "指示ファイル / メモリ" --> C["会話の開始時に読み込まれ、<br/>その後のユーザーメッセージとして届く<br/>= 文脈であって、強制ではない"]
  Q -- "会話に直接打つ" --> U["最優先で処理される<br/>= 進行中の優先順位を組み替える"]
  Q -- "自動テスト / hook" --> H["決まった時点で必ず走る<br/>= AI が何を決めようと適用される"]
  C --> R1["読まれたときだけ効く"]
  U --> R1
  H --> R2["読まれなくても落ちる"]

CC BY 4.0

仕組み: ルールを 1 条ずつ、自動テストに翻訳する

指示ファイルやメモリにある「〜しないこと」を選び、破られた状態を機械が検出できる形に書き直して、テストスイートに足します。例として「サーバーへ書き込む fetch には、必ず CSRF トークンのヘッダを付ける」というルール(付け忘れても手元では動いてしまい、AI が新しい呼び出しを書くときに落としやすいタイプ)を翻訳するとこうなります。

# test/reference/rule_csrf_token_test.rb(実物からの簡約版)
class RuleCsrfTokenTest < ActiveSupport::TestCase
  test "JS that sends non-GET requests also builds the CSRF header" do
    files = Dir.glob("app/javascript/**/*.js")
    non_get = files.select { |f| File.read(f).match?(/method:\s*["']?(post|put|patch|delete)/i) }
    missing = non_get.reject { |f| File.read(f).match?(/X-CSRF-Token/i) }
    assert_empty missing, "a non-GET fetch without the CSRF header: #{missing.join(', ')}"

    # ⚠️ 対象 0 件で緑になるのを防ぐ。自動テスト自体が壊れたら、ここで落ちる
    assert_operator files.size, :>, 150, "too few files scanned (is the glob broken?)"
  end
end

私の手元の本番プロジェクト——typingtube という、YouTube の音楽動画でタイピング練習ができる Web サービスです——では、この形でメモリ上の行動ルール 8 件を自動テストへ移しました。上の CSRF トークンの例も、そのうちの一つです。移したルールのメモリ側は「検査が見ている」という 1 行に縮み、AI がその 1 行を読んでいなくても、破ればテストが落ちて差し戻されます。

書き方の急所は二つです。

一つ、自動テスト自体が空回りする形を塞ぐ。上のコードの最後の行がそれで、「走査対象が N 件以上あった」ことを必ず併置します。glob のパスを打ち間違えた自動テストは、0 件を走査して永遠に合格です。落ちたことのない自動テストは、守っている自動テストと壊れている自動テストの区別が付きません。

二つ、全部を今すぐ直せないルールはラチェットにする。既存コードに違反が 40 件あるなら、「40 件以下」を基準値としてファイルに記録し、増えたら落ちる自動テストにします。新しい違反だけが止まり、積み残しの解消は急かされません。減ったら基準値ファイルの数字を下げて更新すれば、改善が巻き戻ることもなくなります。

移せないものは、移さない

正直な線引きを一つ。自動テストに翻訳できるのは機械が真偽を決められるルールだけです。「リリースより品質を優先する」「この機能はあえて作らない」のような判断・方針は翻訳できないので、読ませる側に残します。私の環境でも、移せたのは 8 件で、方針の類はメモリに残っています。ルールを全部消せるという話ではなく、機械に持てるものを機械に持たせると、残った方針だけになって薄まらなくなるという話です。

CC BY 4.0 はここまで

読まなくなったもの

ルール文書です。AI が読んでいなくても、新しく入った人が読んでいなくても、破れば同じ自動テストが同じ場所で落ちます。「ルールを周知する」「リマインドする」「守られているか目視で確かめる」という仕事がまとめて消えました。人にも AI にも同じガードレールが立つので、チームでは言って回らなくて済むぶん、個人開発より効きます。

CC BY 4.0

検証手順: 破って、落ちることを確かめる

前提

  • ルールを 1 条、自動テストへ翻訳し終えていること
  • その自動テストが、今は通っていること
  • 作業中の変更が無い状態(git status が空)から始めること

所要時間: 10 分

手順

  1. あなたが、そのルールへの違反を 1 つ、わざと作ります(本文の例なら、CSRF トークンのヘッダを付けない fetch を 1 つ書きます)。⚠️ 本物のコードに書いてください。 自動テストが走査している範囲の外に書くと、この検証は何も確かめられません
  2. あなたが、その自動テストだけを走らせます(全量は要りません)

合格条件(すべて満たすこと)

  • 自動テストが落ちる
  • 失敗のメッセージに、手順 1 であなたが書いたファイル名と行番号が出ている

合格しなかったとき

  • 落ちたが、場所が出ない —— 直す側(AI でも、あなたでも)が、毎回そこを探すことになります。メッセージに違反箇所を含める形へ直してから、手順 2 をやり直します
  • そもそも落ちない —— 走査の対象(glob のパスなど)が、あなたが書いた場所を含んでいません

後始末

  • 手順 1 で書いた違反を消し、同じ自動テストをもう一度走らせます。合格に戻ることと、git diff が空になることを目で確かめます

破ったのに落ちない自動テストは、無い自動テストより悪いです。次のセッションの AI は、その合格を「このルールは守られている」と読みます。 確かめ直す理由がないので、違反を書いたまま先へ進みます。そしてこの回がやったのは、ルール文書を読まなくて済む形にすることでした。読まなくてよくなった根拠は、機械が見ていることだけです。見ていない機械が返す合格は、その根拠のほうを先に消します。


CC BY 4.0 はここまで

次回は「共有メモリを整理しない技術」。並列で走るセッションが共有するメモリを、私は整理していません。それでも、そのメモリは適切に機能しています。

連載「読まない技術」