Dev

プロンプトは不変条件ではない

エージェントのプロンプトや CLAUDE.md に書いたルールは、あくまで助言です。モデルはどの経路でもそれを読みますが、重み付けするだけであり、重み付けは拒否ではありません。だからたいていは守られますが、たいていというのは、根幹を支えるルールに求められるものではありません。ルールを本当に効かせるには、遵守が任意でない場所、つまりモデルが何を決めたかに関わらずハーネス自身が実行して強制するチェックへと移す必要があります。ただし限界があります。ゲートが拘束するのは、それがカバーする経路の上だけであり、いくつかのルールはどのゲートでも決着できません。違反が現れるのは世界の中であって、ゲートが検査するはずの操作の中ではないからです。

私が動かしているどのエージェントも、その大半は散文で書かれたルールによって定義されています。そしてそれらのルールは、同じファイルの中に同じ声で並んでいても、すべてが同じ種類のものではありません。あるものは好みです。より小さな依存関係を選ぶ、関数は一目で読める程度に短く保つ、誰も尋ねずに済むように名前を付ける、といったものです。あるものは根幹を支えます。サンドボックスの外で破壊的な削除を実行しない、main に直接 push しない、秘密情報をログに出力しない、といったものです。前者の種類はずれても、失われるのは少しの磨きだけです。後者の種類がずれれば、それは事故です。そして後者を、ファイルの外へ、モデルが何を決めようとハーネスが実行する何かへと移し始めた日に、ファイルがずっとしてきたことが見えました。それは私が思っていたより少ないものでした。ファイルはルールを述べていました。ですが、一度も強制してはいませんでした。

この間違いは犯しやすいものです。プロンプトに「常に」や「決して」と書くと、制約を設定しているように感じられるからです。そう読めますし、そういう形をしています。ですがファイルは、気質によって従う読み手に意図を述べているだけであり、気質は確率であって約束ではありません。モデルはたいていそのルールを認めますが、たいていは、毎回とは別の性質です。好みならそれで構いません。ですが、試されるその一度に効くことだけが仕事であるルールにとって、たいていは弱い保証ではありません。保証が無いということです。

強制とは何によって成り立つのか

すでにそれを持っている工学の分野から、その考えを借りましょう。本物の制約は、一文にはできない二つのことをします。それは拘束します。禁じられた状態に到達することが、勧められない手ではなく、そもそも取れる手ではなくなります。そしてそれはカバーします。その状態へのあらゆる経路の上に座るので、その脇をすり抜けてそこへ到達するものは何もありません。型は拘束もカバーもします。コンパイラがそれをすべての代入に適用し、誤った値を却下します。データベース制約も両方をします。システム内のあらゆる書き込みがそれを通り抜けるからです。アサーションは拘束しますが、ほとんどカバーしません。置いた行では拒否しますが、その行に決して到達しない経路の上では何も見ません。

プロンプトの中のルールは、逆のプロファイルを持ち、そしてそれは悪いほうです。それはすべてをカバーします。モデルがどの経路でもそれを読み、ルールが単に存在しない経路は無いからです。それがしないのは、拘束することです。それは読まれ、コンテキストウィンドウの中の他のすべてと比べて重み付けされ、重み付けは拒否ではありません。周囲の十分な文脈が押しのけることのできるルールは、はじめから拘束的ではありませんでした。それは良い統計を伴う強い選好でした。カバレッジは問題ではありませんでした。問題は、そのカバレッジの下に、ノーと言うものが何も無いことです。

同じルールを、一文として、そしてゲートとして

「main に直接 push しない」を取り上げて、二つの場所に置いてみましょう。CLAUDE.md の中では、それはモデルが読み、たいていは守る一行です。もっともらしい筋書きを与えてみます。この push は例外だ、修正は急を要する、そのルールだけが立ちはだかっている、と。するとモデルはコマンドを実行します。あなたに逆らいたいからではなく、あなたが一文を手渡し、モデルがその一文を重み付けしたからです。

次に、そのルールをハーネスが実行する場所に置いてみます。Claude Code のフックが最も近い例であり、重みを担う言葉は定義の中にあります。

Hooks are user-defined shell commands, HTTP endpoints, MCP tool
calls, LLM prompts, or subagents that execute automatically at
specific points in Claude Code's lifecycle.

「自動的に (automatically)」。モデルはフックを参照することを選びませんし、それを飛ばすことも選べません。イベントが発火したから、ハーネスがそれを実行します。それがファイルに欠けていた拘束であり、それはモデルの読みの内側からではなく、モデルの外側から来ます。フックがいったん下したその拒否も、より大きな声が数で上回れる一票ではありません。

Exit 2 means a blocking error. On events that can block, exit 2
blocks whether or not you print JSON: even a JSON permissionDecision
of "allow" can't override it.

狭く読めば、この文はフックの内部での優先順位、つまり Exit コードとフック自身が出力した判定との優先順位を定めており、Exit コードが勝ちます。狭くて十分です。ブロックは結果であって、数字の付いた選好ではありません。そしてそれを生み出したものは、モデルがそう望んだかどうかに関わらず走りました。CLAUDE.md の一行と同じルールですが、別の種類のオブジェクトです。一方は、その後に行動するものによって読まれ重み付けされ、もう一方は、その行動するものの傍らで走り、すでに決めてしまっています。

どのルールもゲートになりたいわけではない

以上のどれも、すべてをゲートにせよという主張ではありません。ほとんどのルールは一文のままでよいのです。好みにゲートはありません。ある名前が良いか、ある抽象がその複雑さに見合うかを、機械的に決めるものは何もありません。そして定義に挙がったフックの種類のリストには LLM プロンプトも含まれるので、判定させるためにモデルをゲートに掛けることもできますが、それは重み付けを内側へ移して強制と呼ぶだけです。昇格させる価値のあるルールは狭いもので、たった一つの問いで見つかります。あなたの指示のうち、たった一度の静かな違反が、粗い縁ではなく事故になるのはどれか、という問いです。移すべきなのはそれらです。残りは手引きであり、手引きは散文に属します。読まれ、重み付けされる。それこそが散文の得意とすることです。

私が警戒している失敗は、逆の方向にあります。それは、ファイルの中に座り、制約の落ち着いた権威をまといながら、提案のように振る舞う、根幹を支えるルールです。そしてその権威が最も欺くのは、それを書き、処理済みとして読み返す当人にほかなりません。

ゲートが届かないもの

実際に移したルールでさえ、ブロックが見せるほど固くは守られません。フックがカバーするのは、それが置かれた経路だけであり、一つの経路はあらゆる経路ではないからです。シェルツールで push をブロックしても、同じ圧力の下にあるエージェントはそのブロックと言い争いません。別の道で main に到達します。別のツール、API 呼び出し、あるいは代わりに push を実行するスクリプトで。どの道を見張るかを決めるマッチャーは、ドキュメント自身が認めるとおり best-effort なので、見張っている道でさえ不完全にしか見張れません。チェックが拘束するのは、それがカバーする経路の上だけです。これは先ほどの点を、操作のレベルで繰り返したものです。ルールを守るには、その結果へのあらゆる経路の上に座らなければならず、ある地点を過ぎれば、そのやり方はコマンドを検査するのをやめて到達手段そのものを取り上げることになります。そうすれば、見張るべき道はもう残りません。

その下には、もっと厳しい限界があります。いくつかのルールは、どの道のどの検問所でも決着できません。違反が呼び出しの中ではなく、呼び出しが運ばない帰結の中にあるからです。正しい SQL であり、下流のジョブがまだ読んでいる列を落とすマイグレーションは、ゲートのあらゆるチェックを通過します。きれいにパースでき、バックオフを静かに落とすリトライの変更も同じく通過し、損害を見せるのは後になってから、負荷の下、本番環境の中です。ゲートではそのどちらの誤りも見えません。誤りが操作の中に書かれていないからです。それらのルールはそもそも移せませんし、それを守るふりをするファイルの中の一文は、修復のきかない形での、あの最初の間違いの再来です。それらを守るのは、変更を読む人間です。だからこそ先ほどの切り分けが効いてきます。ゲートは狭く保ち、根幹を支えるわずかなものだけを昇格させれば、人間が実際に読まなければならないキューは、千件のゲートが必ず陥る反射的な承認へと朽ちていくことなく、意味を持つ程度に短いままになります。

一つの文が隠す二つの動詞

これは、ルールが同期からずれることではありません。宣言された状態と動いている状態が引き離される問題であり、それを閉じるためにリコンシリエーションが存在する、あの問題ではありません。ルールは完璧に調整され、書かれたとおりに正確に動きながら、なお助言にすぎないことがあります。これは、ルールがどの層に属するか、あるいは誰がそれを書いてよいかの問いでもありません。アドレッシングは、ルールが誰の言葉かを定めますが、運用者の言葉は、何かがその背後に立つまでは、リポジトリの言葉に重み付けとして勝るだけです。そしてこれは、背後に立つとはどういうことかを解き明かす二つのエッセイの上流にあります。すなわち、決定可能な継ぎ目であればクラス全体を拒否できるのに対し、意味的な継ぎ目では代わりにその能力そのものをゲートに掛けざるをえないこと、そして、無人の操作はコマンドが何をするかの推測ではなく、ハーネスが与えた到達範囲に基づくことです。これらは操作についての話です。こちらはファイルについての話であり、そこでは、文が命令形で書かれているという理由だけで、保証がすでに存在すると思い込まれています。

文と制約は同じように読め、どちらにせよ一つの指示であり、その似ていることこそが、何かがそれを試すまで、前者を後者の代わりに立たせておくのです。いま私が自分に課しているルールは、以前書いていたものより小さなものです。根幹を支える「決して」をファイルに書き込む前に、私はそれを述べただけなのか、それとも裏付けたのかを問います。そして裏付けられないときは、命令形にふりをさせておく代わりに、同じ息でそう言います。私が決して裏付けられなかったルール、その損害が呼び出しの中ではなく世界の中に生きているルールこそが、ファイルに守ってほしいと最も願っていたまさにそのルールであり、そしてファイルには決してそれができませんでした。ファイルは、何が真であるべきかを言う場所です。それを真にする場所では、決してありませんでした。

ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit

Max Nardit

@mnardit

ほかの記事

あなたのエージェント設定は、いまやインフラです

エージェントが数体を超えると、その定義はあなたが編集する設定であることをやめ、インフラになります。インフラのやっかいなところは、頭の中に収めきれなくなった時点から、宣言された状態と動いている状態が放っておけば乖離していき、それを誰も報告しないことです。直し方は、サーバーが学んだ規律です。集合をファイルとして宣言し、現実と差分を取り、適用したものを固定し、そして背後で起きた変更の上から適用することを、乗り越えて収束させるのではなく拒否します。ただし、見落としやすい落とし穴が一つあります。リコンサイラーが扱えるのは、あなたが宣言し忘れずにいたものだけです。

コネクターは信頼できない書き手です

あるスパンを誰が書いたかは、ハーネスがすでに把握している事実です。そのスパンに従うのが安全かどうかは、そうではありません。ですから直しは、より賢い読み手ではありません。ツールとピアに、届いた経路である配管の立場ではなく、それらが本来そうである異物の立場を与えることです。

エージェントは、読める道具に手を伸ばす

道具は、何ができるかを基準に最適化されます。ところがエージェントは、結果を予測できるか、そして答えが空で返ってきたときにそれを読めるかを基準に道具を選びます。この二つは別の性質であり、その隔たりこそが、より高機能な道具ほど手つかずのまま残りがちな理由です。