AI エージェント

注意力ではなく設定で固める — コーディングエージェントのスコープと権限ガードレール

複数リポジトリを横断する AI エージェントの作業を、スコープと権限の宣言ファイルで強制する。セッション単位のガードレール設計と、権限定義そのものを触らせない自己制約、push を物理的に固める二重防御。

Read in English

コーディングエージェント(Claude Code のような)に作業させるとき、本当に効いてくるのは複数のリポジトリを横断する場面です。あるリポジトリの実装を参照しながら別のリポジトリに書く。共通ライブラリを直しつつ、それを使う側のリポジトリで動作を確かめる。設定リポジトリを読みながら、生成物のリポジトリを更新する。単機能の修正よりも、こういう「またがる」作業のほうがエージェントの速さが活きます。

ところが横断作業は、同じだけ事故の口も広げます。参照するだけのつもりのリポジトリをうっかり編集する、別リポジトリの main に直接 push してしまう、目的と関係ないファイルに手を入れる。リポジトリをまたぐほど「いまどれを触ってよくて、どこまで許されているか」の境界が曖昧になり、エージェントはその境界を毎回ゼロから読み直すので、人間の注意力に相当するものが構造的に無いのです。だから「プロンプトで注意深くお願いする」では根本的に防げません。

そこで、注意力ではなく設定ファイルでガードレールを強制する仕組みを組みました。考え方は単純で、作業を始める前に「またいでよいリポジトリ」と「それぞれの権限」を宣言し、それ以外は技術的に不可能にしておく、というものです。横断する範囲そのものを、最初に設定として固定してしまう。

セッション = スコープ + 権限の宣言

セッションがスコープと権限を宣言し、横断する複数リポジトリを read / write-local / write-push の異なる権限で扱う構成図

親がスコープと権限を宣言してセッションを生成する。横断する各リポジトリは権限ごとに扱いが変わり、「読むだけのリポジトリ」は構造的に書き換えられない。

最小単位を「セッション」と呼んでいます。1 セッション = 1 つの作業単位で、起動する時点で次の 2 つが確定します。

  • スコープ … どのリポジトリ / ディレクトリを触ってよいか
  • 権限 … 対象ごとに、どこまで許すか

権限は 3 段階です。

  • read … 参照のみ
  • write-local … 編集 + ローカルコミットまで。push 不可
  • write-push … push まで許可(PR 作成を含む)

ポイントは、権限を対象ごとに持てることです。横断作業では、1 セッションの中で信頼度の違うリポジトリが当たり前に混ざります。「設定リポジトリは read で参照するだけ、生成物のリポジトリは write-push して反映する」「共通ライブラリは write-local で直すが push はしない、利用側リポジトリは read で動作確認だけ」——こうした組み合わせを 1 つの作業単位で扱う。だからセッションは、対象とレベルの組を並べた宣言(manifest)を 1 枚持ち、スコープ・権限・同一性判定をすべてそこに集約します。

実際、この記事もそういう横断作業で書いています。仕組みの出どころであるリポジトリを read で参照しながら、文章はブログのリポジトリに write-push で書く。読む側を絶対に書き換えない、という保証が設定で効いているので、参照元を壊す心配をせずに済みます。

スコープは「開いていないものは見えない」で作る

スコープは許可リスト方式です。エージェントに渡すアクセス可能ディレクトリの一覧に、許可した対象だけを列挙します。列挙されていないリポジトリは参照すらできません。シンボリックリンクを張る方式は採らず、設定ファイルだけでスコープが完結するようにしています。

「触れるものを足していく」のであって「触れるものから引いていく」のではない、という向きが大事です。デフォルトが閉じているので、設定を書き忘れた対象は安全側(=見えない)に倒れます。

山場 1:権限定義そのものを、エージェントに触らせない

ここが一番効いている設計判断です。セッションの中から、スコープと権限の定義ファイル自体を書き換えられないようにする。

理屈はこうです。エージェントに read 権限を渡すとき、その「何が read なのか」を書いた設定ファイルもまた、ファイルです。もしそれを編集できてしまうと、エージェントは「この deny を外せば書ける」と気づいて、自分でガードレールを緩める余地が生まれます。権限を持つ主体が、自分の権限定義を書き換えられてはいけない。だから権限の変更は必ず一段上(セッションを生成する親)からしか行えない構造にしました。

ただし、ここには実装上の罠があります。守りたいのは「権限とスコープの定義ファイルそのもの」です。だからといって「Edit / Write を丸ごと禁止すればいい」と考えると失敗します。それをやると、エージェントは自分の作業ログすら書けなくなる。必要なのは「ツール単位の禁止」ではなく「特定パスだけの禁止」です。守りたい定義ファイルだけを名指しで deny し、セッションの作業領域は書ける状態を保ちます。

// 守りたいのはセッション自身の権限/スコープ定義。そこだけを名指しで禁止する
"deny": [
  "Write(.claude/**)",   // 権限定義(settings)
  "Write(/CLAUDE.md)"    // スコープ宣言
  // 作業ログなどセッション内の領域は書けるまま残す
]

一方で、read 権限の対象リポジトリは逆に丸ごと禁止します。読むだけなのだから、そのツリー全体を Write / Edit の対象から外すのが素直で安全だからです。問題はこの「丸ごと禁止」と、さっきの「自分のログは書けるようにする」がぶつかったときに起きました。

実際に一度しくじっています。セッション自身の作業ディレクトリを、read 対象のリポジトリの配下にネストして置いていたのです。read 対象をツリーごと禁止した結果、その中にあったセッションのログまで巻き添えになり、エージェントが自分のログを書けなくなりました。

ここで効いたのは「deny を狭める」ことではなく、作業領域とスコープ対象を物理的に分けることでした。セッションの作業ディレクトリ(ログや一時メモ)を、そもそも保護対象のツリーのに出してしまう。そうすれば read 対象は安心して丸ごと禁止できるし、セッションは自分のログを自由に書ける。守る範囲を削って辻褄を合わせるのではなく、守る対象と書き込む場所が重ならないように土俵を分ける、という解き方でした。

山場 2:push は二重に固める

一番こわいのは push です。read のリポジトリへ push する、main へ直接 push する。どちらも取り返しがつきにくい。

素朴には、push を許すコマンドの許可リストを絞れば止められます。これはエージェントのうっかり事故にはよく効きます。が、固さとしては不十分です。許可リストは「git push という形のコマンドかどうか」しか見ないので、git -C <別リポジトリ> push のように作業ディレクトリを切り替える変種や、どのリポジトリへ・どのブランチへ push しようとしているかまでは判定できません。コマンド形の照合だけでは、行き先の権限までは守れないのです。

そこで 2 段にしました。

  1. 許可リストで「push できる対象」を絞る … push を許可するのは write-push を持つセッションだけ。readwrite-local しか持たないセッションには、そもそも push コマンドを与えない。
  2. 実行直前フック … コマンド文字列を検査し、push 先リポジトリのパスから権限を引いて判定する。

固いのは第二段です。実行されようとしているコマンド文字列そのものを見て、git -C <path> や作業ディレクトリから push 先のパスを割り出し、manifest(パス→権限)に照らします。read / write-local のパスへの push は全部ブロック、write-push のパスでも main / master への直 push はブロックする。push 先を特定できないときは安全側に倒して拒否し、「対象を git -C <絶対パス> で明示せよ」と促します。コマンド形の許可では届かない「行き先ごとの権限」を、フックが物理的に固める形です。

切れても再開できるようにする

エージェントのセッションは切れます。文脈ごと消えると毎回ふりだしに戻るので、作業ログを 2 層で残しています。

  • スナップショット(常に最新・上書き)… 「まずこれだけ読めば再開できる」1 枚。現在のタスク、対象リポジトリと作業ブランチ、触ったファイル、次の一手、未解決の判断事項
  • タスクログ(時系列・追記)… 経緯と決定理由を残す履歴

そして再開時、起動フックでスナップショットを自動的にコンテキストへ注入します。エージェントは何も指示しなくても「続きから」始められる。ガードレールが事故を防ぐ仕組みなら、こちらは作業を失わない仕組みです。

いまの位置と、これから

正直に書くと、これはまだ個人用です。自分のマシンで、自分のエージェントにガードレールをかけるために組んだもので、本番運用システムではありません。

ただ「セッション = スコープ + 権限の宣言」という形そのものは、個人に閉じる必要がありません。今後はリポジトリとして公開し、グローバルにインストールして誰のマシンでも同じようにセッションを切れるようにすることを検討しています。コーディングエージェントを日常的に使う人が増えるほど、「エージェントに何を触らせ、何を触らせないか」を宣言として外側に固定する需要は出てくるはずで、その共通の足場になればと思っています。

エージェントを賢く制御しようとすると、つい賢いプロンプトを書きたくなります。でも事故を本当に止めるのは、たいてい賢さではなく「そもそも不可能にしておく」設定の方でした。

コメント

コメントを読み込んでいます…

コメントできない場合は GitHub で開く ↗