こんにちは。認証チームのいまひろです。
前回は、チームで使ってきた汎用の既製ツールを、生成 AI に実装させた小さな専用ツールで置き換えた話を書きました。
前回の主眼は、生成 AI で「作るコスト」が激減し、一方でサプライチェーン攻撃によって「既製ツールを使い続けるリスク」が上がった——という、Build vs Buy の前提そのものの変化でした。そして専用ツールの利点として挙げたのは、機能過多ゆえの設定の煩雑によるリスクが消えることと、使わない機能の分だけ広かった攻撃対象領域が縮小されること、いずれもツールそのものの性質についてでした。
その後ツールを増やしていく中で、まったく別の角度の利点に気づきました。専用ツールは、AI エージェントに何をさせるか——その範囲を決める「ハーネス」を担わせる場所としても適している、ということです。
要約すると——「これは実行するな」と指示として書くのではなく、「そもそも実行できない」形にツール側へ落とし込む。汎用ツールや汎用 API を経由している限り難しかったことが、専用ツールを挟むことで素直に実現できる、というのが本記事の主張になります。
※ ハーネス(harness)は本来「馬具」を指す言葉ですが、AI エージェントの文脈では、モデルを実際に動かすための足回り全体を指して使われます。Anthropic の定義では「モデルの周囲にあるソフトウェアの足場(scaffolding)——生の知能を動作するエージェントに変える、ループ・ツール・コンテキスト管理・ガードレール」とされています(Agent Harness Design)。本記事ではそのうち、エージェントが動ける範囲を機械的に限定する部分(ガードレール)に焦点を当てて「ハーネス」と呼んでいます。
エージェントは「指定していない手順」も実行しうる
従来の CLI コマンドやプログラムは、書かれたことしか実行しませんでした。一方、Claude Code のようなコーディングエージェントは、曖昧な指示から意図を汲み、手順を組み立て、失敗すれば別の手を試してくれます。
ただ、これは表裏一体で、「意図を汲んで手順を組み立てる」ということは、「こちらが指定していない手順を実行しうる」ということでもあるのです。
たとえば「新しい版をアップロードしておいて」と頼んだとき、同じ名前の古いファイルが残っていれば、エージェントは「紛らわしいから消しておこう」と判断することがあります。整理という意味では筋の通った判断ですが、こちらは削除までは頼んでいません。また、調査を頼めば、原因に近づくために自分で調査範囲を広げ、こちらが想定していなかった情報を参照しに行くこともあります。
いずれも悪意はまったくなく、むしろ「言われた仕事をきちんと完遂しよう」とした結果としての行動です。目的達成に向けて手段を自分で選ぶからこそ、こちらの想定の外側に手が伸びる。エージェントの有能さと、この種のリスクは同じ性質から表出しています。
もう一つ、構造的な問題があります。手元のターミナルで動かすエージェントは、たいてい開発者本人の権限で動くということです。本人には組織が役割に応じた権限を与えていますが、その本人が起動したエージェントに同じ範囲をまるごと渡したいかというと、話は別です。そのタスクで必要なのは、たいてい持っている権限のごく一部だからです。
ところが権限体系は基本的に「人」に対して設計されており、「同じ人が動かしているエージェントにだけ、より狭い権限を与える」という切り口は、多くのサービスで用意されていません。組織の権限管理の側では「人 対 エージェント」の線を引くことができない。そのため、この線は、エージェントが実際に手を伸ばす場所——ツールの側で引く必要があります。
プロンプトにハーネスを書いても、確実には効かない
最初に思いつくのは、ルールとして書いておくことです。Claude Code なら CLAUDE.md、あるいはスキルの定義ファイルに「本番環境には接続しない」「このプロジェクト以外には書き込まない」と明記しておく。私たちも実際にそうしています。
これは有効ではあるものの、確実ではありません。プロンプトに書いたルールはモデルへの入力の一部でしかなく、文脈次第で「今回は例外的に実行したほうがユーザーの意図に沿う」と解釈される余地が残ります。禁止のつもりで書いた一行が、状況によっては強い推奨程度にしか効かない、ということが頻繁に起きます。
このことは Anthropic 自身が、フック(hooks)の説明の中で明確に言語化しています。
Hooks are user-defined shell commands. Claude Code runs them at specific points in its lifecycle, which gives you deterministic control: certain actions always happen rather than relying on the LLM to choose to run them. (Automate actions with hooks より、強調は筆者)
「LLM が実行を選ぶことに依存するのではなく」——つまり、プロンプトに書いたルールは "LLM が選ぶ" 側だという前提です。確実に効かせたい制約は、フックのようにモデルの外側に置く必要があります。
Claude Code のハーネスと、その苦手な領域
ハーネスをモデルの外側に置く仕組みとして、Claude Code には少なくとも次の 3 つが用意されています(Configure permissions / Security)。
| 仕組み | 何をするか |
|---|---|
| パーミッションルール | ツール単位・コマンド単位で allow / ask / deny を設定する |
| PreToolUse フック | ツール呼び出しの直前に任意のスクリプトを走らせ、引数を見て拒否できる |
| サンドボックス | Bash 実行をファイルシステム・ネットワークごと隔離する |
いずれもモデルの外側にあり有用な手段ですが、「ここは苦手」という領域も存在します。
1. コマンドの引数を制約するのは難しい。
公式ドキュメントは「引数を制約しようとする Bash のパーミッションパターンは脆い(fragile)」と警告として明記しています。Bash(curl http://github.com/ *) で curl を GitHub に限定したつもりでも、オプションの位置(curl -X GET ...)、プロトコルの違い、リダイレクト、変数展開(URL=... && curl $URL)のいずれでもマッチしません。同じことをする書き方が無数にある以上、文字列パターンでの絞り込みには常に穴が残ります。
2. MCP の許可はツール名までしか絞れない。
粒度は mcp__<サーバ名> と mcp__<サーバ名>__<ツール名> の 2 段階で、引数の中身では絞れません。「アップロードツールは使ってよい、ただし特定フォルダ配下に限る」といった指定はできない、ということです。汎用の MCP サーバはサービスの API をおおむねそのまま露出するので、その結果「認証が通る範囲すべて」が操作可能な範囲になります。
3. API トークンの権限分離も、たいてい同じ粒度で止まる。
たとえば Atlassian のスコープは read:jira-work / write:jira-work のようにリソース種別 × 操作の形で定義されており、特定のプロジェクトに限定するスコープは用意されていません。エージェントに渡したい権限の形は「特定リソース × 特定操作」なのに、供給側の粒度は「サービス × 操作種別」で止まっている——この食い違いは頻繁に起こります。エージェント専用のトークンを別に発行しても、切り出せる形が同じなら実際の範囲は狭まりません。
4. フックで補えるが、「検閲」になる。
PreToolUse フックは呼び出し引数を丸ごと受け取れるので、MCP ツールの引数を見て拒否することは可能です。ただしフックがやっているのは、表現力の広いツールの手前に検問所を置き、通ってきたものを一つずつ検査することです。汎用ツールが受け付ける引数の全パターンを把握している必要があり、書き漏らした形はそのまま通ります。「危険な形を列挙して塞ぐ」というアプローチは、危険な形を列挙しきれる前提の上に成り立っています。
専用ツールなら「そもそもできなくする」アプローチを採用できる
汎用ツールを専用ツールに置き換えると、問題の立て方そのものが変わります。危険な操作を検査して弾くのではなく、そもそも表現できないようにするアプローチが取れるからです。
チーム内に配布している、調査レポートを共有ドライブへ置くための CLI(Go の小さなバイナリ)を例にすると、設計判断は次の通りです。
- 配置先はフォルダ ID で固定。設定値として持ち、そこへ直接移動する
- 引数から URL やパスを一切組み立てない。
../などで別の場所へ到達する経路が存在しない - 削除機能を持たない。作成とアップロードだけ
本質的に効いているのは 2 つ目です。パスを引数から組み立てる実装であれば、どれだけ検証を書いても「その検証は十分か」という問いが残り続けます。組み立てる処理そのものが無ければ、その問い自体が消えます。検証の網羅性を証明する代わりに、危険な経路を存在させないわけです。3 つ目も同じ発想で、エージェントが「古い版を消してから上げ直そう」と気を利かせても、選べる手段の中に削除がありません。
要点は、これらが「AI に守らせるルール」ではなくツールの実装だという点です。認証情報を持っているのはツールの側なので、エージェントがどう解釈しようと、フックの判定に漏れがあろうと、実装されていない操作は起こりません。
| 汎用ツール / MCP | 専用ツール | |
|---|---|---|
| 制御の方式 | 禁止事項を列挙して弾く(ブラックリスト寄り) | できることだけを実装する(ホワイトリスト) |
| 制御の置き場所 | プロンプト・パーミッション・フック | ツールのコード |
| 「書き漏らし」の結果 | 通る | 実装されていないので不可能 |
| テストできるか | フック単体のテストは可能 | ユニットテストが普通に書ける |
最後の行は地味ですがかなり有用で、ハーネスがコードになると、ハーネス自体の挙動をテストで担保できるようになります。
作り直すのではなく「包む」
「専用ツールを作る」と言うと、巨大な CLI をゼロから自作する大工事を想像されるかもしれません。ですが実際には、既存の汎用 CLI や API を内部で呼び出すごく薄いラッパーを書くだけで十分なケースも多いかと思います。
このときに効いてくるのが、エージェントからは汎用ツール側を見せないという構成です。Claude Code では、MCP サーバをパーミッションの deny に指定すると、そのツール群はモデルのコンテキストから取り除かれ、エージェントからは存在しないものになります。managed settings(組織配布の設定)に置けば、利用者側の設定で上書きすることもできません。副次的に、選択肢が減るぶん手順の組み立てで迷わなくなるという効果もあります。
ただし、Bash 経由で使う汎用 CLI を同じように「隠す」のは難しいという点は押さえておく必要があります。Bash(aws *) のような deny ルールは先ほど見たとおり文字列マッチだからです。迂回そのものを無意味にしたいのであれば、認証情報をラッパー側だけが持つ形にするのが良いかもしれません。直接叩いても認証が通らないのであれば、迂回する動機自体がなくなるからです。
副次的効果 — コンテキストと時間
ハーネスが主目的の置き換えでしたが、日常的に効かすことのできる副次的な効果もありました。
1 つはコンテキストの節約です。 汎用の API ラッパは、API が返すものをほぼそのまま返します。チケットを 1 件取得しただけで使わないフィールドを大量に含んだ JSON が返り、それがそのままコンテキストを消費します。専用ツールなら返すフィールドをこちらで決められます。
もう 1 つは時間です。 エージェントに汎用ツールを使わせると「考えて、呼んで、結果を読んで、また考える」というモデルの往復が発生しますが、専用ツールなら一連が Bash 1 回になります。判断を伴わない定型部分をツールに閉じ込めた分だけ、推論の往復が消えるわけです。
どこを置き換えるか
判断基準はシンプルで、「エージェントに渡したい権限の形」と「汎用ツールが持っている権限の形」がずれている箇所を候補に挙げることになります。
| 領域 | 汎用ツール / API のままだと | 専用ツールにすると |
|---|---|---|
| 課題管理・ドキュメント管理への書き込み | 認証が通る範囲すべてに書ける | 対象プロジェクト・スペースをツール内で固定できる |
| 監視ダッシュボードの更新 | 更新系 API が定義一式の置き換えになっている場合があり、部分更新のつもりで既存を消す事故が起こりうる | 「追加のみ」「既存には触れない」操作だけを実装すれば、一括削除が表現できない |
| 成果物の共有・公開 | 置き先が引数次第。秘匿値のマスク忘れも検知されない | 置き先を固定し、公開前スキャンを経路として強制できる |
| クラウド CLI の定型操作 | aws などは全サービス・全操作が可能 |
環境・リージョン・対象リソースを固定した、目的別の小さなコマンドにする |
| データストアへの参照系クエリ | 任意のクエリが流せる | クエリを定型化してパラメータだけ受け取る。LIMIT を強制する |
共通するのは、汎用ツールの機能のごく一部しか使っていないのに、権限だけは全部渡しているという構図です。前回「機能の 5% しか使っていないツールは置き換え候補」と書きましたが、残り 95% は使っていないだけで、エージェントからは到達可能なのだと考えると、置き換えの動機がもう一段強くなります。
これは「セキュリティ境界」ではなく「安全装置」
最後に、大事な但し書きを。ここで作っているのは「セキュリティ境界」ではなく「安全装置(セーフティ)」です。専用ツールが防いでいるのは、善意で有能なエージェントが想定外の挙動をすることであって、悪意ある第三者やプロンプトインジェクションによる意図的な迂回ではありません。エージェントが Bash を持っている以上、専用ツールを通らない経路は原理的に残ります。
信頼境界(trust boundary)はあくまでサーバー側の権限設定やトークンのスコープにあり、専用ツールはその内側で「うっかり」を減らす層だと捉えるのが正確です。Anthropic 自身も、Claude Code の各種保護について「リスクは大きく下がるが、あらゆる攻撃に完全に免疫のシステムは存在しない」と明記しています(Security)。
ゆえに、ブランチ保護・IAM ポリシー・読み取り専用の DB ユーザーのように、手元のツールを迂回されても効く場所に置ける制約は、常にそちらが上位になります。専用ツールのガードは、それらの代わりではなく、権限の粒度が足りない部分を補う層として使うべきものだと捉えています。フックやパーミッションとも排他ではなく、多層防御として併用するのが素直なアプローチであると考えています。
そして前回も書いたことの繰り返しになりますが——「AI が書いたから安全」ということではありません。ハーネスとして書いたコードこそ、人間のレビューを通す価値があると思います。
まとめ
生成 AI は、こちらが書き下していない手順を組み立ててくれます。それは大きな利点であると同時に、こちらが想定していない手順も実行しうるというリスクも内包しています。
- 手元のエージェントは開発者本人の権限で動くが、「人 対 エージェント」で権限を分ける手段はサービス側に乏しい
- プロンプトに書いたルールは、確実ではない
- パーミッションルールは強力だが、引数の中身を縛るのは公式に「脆い」とされている
- MCP の許可はサーバ名・ツール名までで、「このプロジェクトだけ」という粒度は表現できない
- フックで補えるが、それは危険な形を列挙して塞ぐアプローチになる
これらを踏まえると、ハーネスを効かせたい箇所にだけ専用ツールを挟むという選択肢が現実的に見えてきます。全体としてはエージェントに自由に動いてもらいつつ、リスクの高い経路だけ「そもそもできない」形に置き換える。すべてをゼロから作り直す必要はなく、置き換え箇所によっては、汎用ツールを包んで、エージェントに見せる面だけを絞るだけの専用ツールから始めるという選択肢を採用することも可能です。
前回の締めくくりで「機能の 5% しか使っていないツールは置き換えを見積もり直す価値がある」と書きました。今回はそこに一行足したいと思います。そのツールに渡している権限のうち、エージェントに本当に必要なのは何割か。その差分が大きいほど、専用ツールに置き換える価値は高いと感じています。