こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトの開発は、AI のコーディングエージェントに作業させる形で進めています。エージェントに渡す「やってはいけないこと」をドキュメントに書いていましたが、読んだうえで踏まれました。そこで、事故になる形のコマンドだけをツールの実行前に機械で止めるように変えました。この記事は、何を止めて何を止めないかをどう分けたかの話です。
前提は 1 つだけです。Claude Code などのエージェントのホストには、ツールを実行する直前にスクリプトを呼ぶフック(PreToolUse)があります。スクリプトが「拒否」を返すとそのツール呼び出しは実行されず、「確認」を返すと人に許可を求めるプロンプトが出ます。
症状 — 読んだうえで踏む
禁止事項は、エージェントがセッションの最初に必ず読むドキュメントに節としてまとめてありました。「データベースのファイルを git で巻き戻さない」もその 1 つです。
それでも、エージェントは git checkout でデータベースのファイルを巻き戻し、収集した 115 件のデータと、その 1 件ごとに AI が付けた判定結果を消しました。ドキュメントには書いてあり、そのセッションはそれを読んでいました。別の作業に集中しているときに踏みます。人でも同じことは起きますが、エージェントは 1 セッションで多くのコマンドを実行するので、踏む確率が積み上がります。
書いてあることと、守られることは別でした。
外れた仮説 — 事故の形を全部「拒否」にすればよい
フックに移すと決めたとき、最初は、デプロイや force push のような外部へ出る操作も含めて、当たった形を全部「拒否」にしました。これは一度採用して撤回しています。
「拒否」は正当な実行まで止めます。デプロイは人が決めて実行するものなので、拒否にすると、実行するたびに回避用の環境変数を前置することになります。回避が常用になると、止めている意味が無くなります。
対処 — 「人が許可したら通るか」で段を分ける
止め方を 3 段と素通しに分けました。分ける軸は「人が許可したら事故でなくなるか」です。
- 拒否のみ — 人が許可しても事故は事故になるもの。全部をまとめてステージするコミット、データベースのファイルの巻き戻し。回避の手段はありません。
- 拒否だが、確認すれば通る — 既定が事故側に倒れているだけで、正当な実行もあるもの。範囲を指定しない
git commit(他のセッションがステージした変更を巻き込むので止めたいが、空のコミットやメッセージだけの直しでは正しい形)、件数の上限を付けない AI の一括呼び出し。人に確認して許可が出たら、環境変数を前置して通します。 - 確認 — 人が判断すべきもので、機械が禁じるべきでないもの。デプロイや force push など、外部へ出る操作です。プロンプトが出て、拒否を押せば実行されません。環境変数を前置しても効きません。
- 素通し — 上のどれでもないもの。シェルとして解釈できない構文や、判定に迷う形もここに入れます。
どの規則がどの段かは、属性として持たせず、判定を呼ぶ位置で決めました。入口の判定ループはこうなっています(コメントは省いています)。
override = any(token == "KODOMO_NEWS_GUARD_OK=1" for seg in raw_segs for token in seg)
for seg in segs:
check_git_add_all(seg)
check_git_discard_db(seg)
check_irreversible(seg)
if override:
continue
check_git_commit(seg)
check_batch_limit(seg)
if override: continue より前で呼ぶ判定は環境変数で外れず、後ろで呼ぶ判定は外れます。「確認」を返す判定も前に置いてあります。プロンプト自体が人の判断なので、環境変数で飛ばす理由が無いためです。判定を 1 つ足すときは、置く位置を決めれば段も決まるので、段を書き忘れることが起きません。
回避の手段は、この環境変数の前置 1 つだけにしました。コマンド行に痕跡が残るので、黙って回避したことにはなりません。
迷ったら素通しにする
判定を書くときの方針も 1 つ決めました。迷ったら素通しにします。
取りこぼしは事故 1 回分の損失です。誤検知は、毎回の作業を止めます。禁止形と同じ文字列を含む検索や、ヘルプの表示や、dry-run の実行が止まると、フック自体が邪魔者になり、回避の環境変数が常用されます。
そこで、判定表には「止まるべき形が止まること」だけでなく、「ドキュメントが指示している形が素通しすること」も入れました。導入時に、ドキュメントに載っているコマンド 73 本を全部フックに通し、止まったのが意図した 18 本だけであることを確かめています。「止めたい形が止まる」より「止めたくない形が通る」のほうが、後から気づきにくいところです。
同じ理由で、復旧に使うコマンドは止めていません。データベースが消えた直後に復旧のコマンドが止まるのが、最悪の失敗だからです。
一般化できる部分
ドキュメントに書いた規則は、破っても何も起きないなら、いずれ破られます。読み手が AI でも人でも同じです。
そのうえで、全部を機械で止めることはできません。線を引くなら、「事故になるか」と「コマンドの形で判定できるか」の 2 つで引きます。判断が要るものは「確認」として人に残します。
フックで止めるようにした規則は、ドキュメントから消しました。同じ規則を 2 か所に持つと、直すときに片方だけが直ります。
まとめ
- ドキュメントの禁止事項は、読んだうえで踏まれる。実行前のフックに移すと、規則が「読んで守るもの」から「実行できないもの」になる
- 全部を「拒否」にすると正当な実行まで止まり、回避の環境変数が常用される
- 止め方は「人が許可したら事故でなくなるか」で段に分ける。拒否のみ・確認すれば通る拒否・確認・素通し
- 段は属性ではなく、判定を呼ぶ位置で表す。足すときに書き忘れが起きない
- 迷ったら素通しに倒す。誤検知は毎回の作業を止める
- 判定表には「止めたくない形が通ること」も入れる。復旧のコマンドは止めない
- フックに移した規則はドキュメントから消し、正本を 1 つにする