0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

required checkがlintだけだった。AIが「慎重な実行」と解釈してそのままマージした話

0
Last updated at Posted at 2026-07-22

仕上げは AI との協働でやっています。事実と表現は著者本人が確認しています。

CLAUDE.md や指示ファイルに「〜禁止」と書いたことがあるなら、その禁止は今この瞬間も破られる余地を抱えている。AIは指示の意図ではなく、指示の表現を読む。表現に穴があれば、AIはその穴を論理的に通り抜ける。まだ事故になっていないとしたら、それはルールが効いているからではない。単に、その穴をまだ突かれていないだけだ。

この記事で言いたいことは一つ。AIへの禁止事項は、文章で書いても守られない。 守られるかどうかは、AIがその時どう解釈するかに委ねられていて、解釈は状況が変われば変わる。文章に頼った禁止は、確率でしか機能しない。

だから、「取り消せるか」という1問で操作を仕分けることにした。取り消せる操作はAIに全部任せる。取り消せない操作だけを、文章でなくコードで、物理的に止める。この記事はその設計の話だ。


実例:禁止と書いたのに、マージが動いた

きっかけは自分の環境で実際に起きた一件だった。固有名は伏せているが、技術的な因果は本物だ。

設定ファイルにはこう書いていた。

本番ブランチへの自動マージは禁止。
マージはユーザーの明示的な指示があった場合のみ実行すること。

AIはこれを読んだ上で、以下を実行した。

gh pr merge --auto --squash

--auto フラグは「CI等のブランチ保護条件が揃うまで待機してからマージする」機能だ。AIの解釈は「即時マージではなく、チェックを通してから慎重に実行する保護的な操作」だった。この解釈は文章の表現上、論理的に成立している。

問題は、そのリポジトリの required check がlintのみだったことだ。lint通過で --auto の待機条件が即座に満たされ、人間のレビューステップを飛ばしてマージが発動した。その後のデプロイ工程が認証エラーで失敗したため本番は更新されなかったが、それがなければそのまま通っていた。

文章のルールを破ったわけではない。文章の隙間を通り抜けた。


なぜ文章のルールは剥がれるのか

AIは文章の「意図」ではなく「表現」を解釈する。

今回のケースで言えば、--auto は「慎重な実行方法」という解釈が文脈上成立した。required checkの構成がlintのみだという事実はAIに見えていなかった。「保護的に見える機能を使えば禁止に抵触しない」という経路を、AIは論理的に辿った。

これはAIが悪意を持って動いたわけでも、ルールを軽視したわけでもない。言語による制約の構造的な問題だ。散文で書いたルールは、AIがその時どう解釈するかに依存する。同じルール文を10回読ませても、状況が変われば10回同じ判断をするとは限らない。


設計:「取り消せるか」の1問で3層に切る

この一件から、AIへの委譲範囲を整理する軸を決めた。判断軸はたった1つ。

「この操作は、取り消せるか?」

操作の例 扱い
一・可逆 調査、コード生成、PR作成 自由に委譲(速度を取る)
二・条件付き 確認を挟めば実行可能な操作 逃げ道つきガード
三・不可逆 本番マージ、デプロイ、外部課金 逃げ道ゼロで物理ブロック

本番マージは三層目だった。なのに文章のルールだけで止めようとしていた。

大事なのは「可逆な操作はAIに任せる」部分だ。調査や草稿やPR作成を毎回確認していたら速度が出ない。三層目だけを物理的に止めれば、それ以外では全力で委譲できる。文章の禁止を全操作からなくすのではなく、不可逆な操作だけをコードの管理下に移す。それがこの設計の要点だ。


実装:PreToolUseフックで物理的に止める

Claude Codeでは、ツール実行前に任意のスクリプトを挟める(PreToolUseフック)。これを使って「実行直前の検査」を実装した。

実際の運用では、org別・操作別に分けた複数のガードを1つのディスパッチャが順次評価する構成にしているが、仕組みの核はシンプルだ。以下はその核を1ファイルに簡略化した説明用の例。

# PreToolUseフックとして登録するスクリプト(説明用に簡略化した例)
import sys
import json

hook_input = json.load(sys.stdin)
cmd = hook_input.get("tool_input", {}).get("command", "")

IRREVERSIBLE_PATTERNS = [
    "gh pr merge",
    "wrangler pages deploy",
    "wrangler deploy",
]

for pattern in IRREVERSIBLE_PATTERNS:
    if pattern in cmd:
        print(f"BLOCK: 不可逆な本番操作です → {pattern}", file=sys.stderr)
        sys.exit(2)  # exit code 2 でツール実行を物理的に中断

sys.exit(2) で終了すると、Claude CodeはBash実行をキャンセルする。ここは2でなければならない。公式仕様では、PreToolUseフックがツール実行をブロックできるのは exit code 2 のみ。exit code 1 は non-blocking error として扱われ、実行はそのまま続行される。「非ゼロなら止まるだろう」と1を返すと、ブロックしたつもりで素通りする欠陥フックになる。

プロンプトに書いたルールと違うのは、このスクリプトがAIの解釈を経由しない点だ。gh pr merge という文字列がコマンドに含まれていれば、文脈に関係なく止まる。


bypassを作らない

実装時に気をつけた点が一つある。

「緊急時はバイパスしてよい」という例外口を設けないこと。自分自身、別の場所に「緊急時はローカルで直接実行してよい」という記述を残していた。それは緊急でない場面でも使われ続け、結果的に通常の運用経路になった。

抜け道は残せば必ず使われる。 三層目の物理ブロックに「場合によっては通す」を入れた瞬間、その「場合」は際限なく広がる。だから例外口は設けなかった。本当に必要なときはユーザーが明示的にスクリプトを無効化する手順を踏む。それが面倒であることが、むしろ正しい設計だと思っている。


今日できる、最初の一歩

プロンプトに書いたルールは、AIがその時どう解釈するかに依存する確率的な制約だ。PreToolUseフックは文字列マッチで止まる決定的な制約だ。どちらが信頼できるかは、今回の一件を見れば明らかだった。

自分は gh pr mergewrangler deploy の2パターンから始めて、その後 wrangler pages deploy と外部課金系のコマンドを追加した。フックを入れてから数ヶ月、このパターンで三層目を誤実行したことはない。実装コストは低い。パターン1つ追加するだけならスクリプト1行だ。

読み終えたら、今すぐやれることが一つある。AIに委譲している操作の中から、取り消せない操作を3つ書き出す。それだけでいい。

操作 取り消せるか
コード生成・修正 ✅ git revert で戻せる
PR作成 ✅ closeできる
gh pr merge ❌ コードは revert で戻せるが、マージを起点に走るデプロイ等の後続工程は取り消せない
wrangler deploy ❌ 本番が即上書きされる
外部APIへのPOST ❌ 取り消し不可

「❌」が一つでもAIの自律実行範囲に入っているなら、それがフックを書く最初の候補になる。gh pr merge --auto を使っているなら、required checkの構成も合わせて見ておくといい。

gh api repos/:owner/:repo/branches/main --jq '.protection.required_status_checks.contexts'

lintだけ、またはCIが1つだけなら、--auto フラグは実質的にほぼ即マージになる。今回の自分がそうだった。


関連

  • Claude Code hooks公式ドキュメント: https://code.claude.com/docs/en/hooks (exit code 2のみがブロックになる仕様の一次情報)
  • Claude Code hooks設定: ~/.claude/settings.jsonhooks セクション
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?