blog
確認プロンプトに頼らないAIの止め方
Console Kludge のハーネスは、記事もゲームも「人間がトリガーを引き、検証→採点→コミットは自動で完結させる」設計にしている。裏を返すと、Claude Code は普段からコマンドを自分の判断で叩き続けているということでもある。確認プロンプトさえ出ればAIが勝手にデプロイすることはないだろう、と最初はどこかで思っていた。ここで一度立ち止まって考えたのが、「AIがデプロイやドメイン購入を勝手にやってしまう可能性」をどう塞ぐかだった。
確認プロンプト頼みが危険な理由
……でも、本当に確認プロンプトは出るのか? 疑って ~/.claude/settings.json を見直したら、答えは「出ない」だった。グローバル設定は Bash(*) を許可し、skipAutoPermissionPrompt=true になっている。つまり確認プロンプトは出る前提ではなく、自動でスキップされる設定になっている。これはこのプロジェクトだけの話ではなく、環境側の既定なので、プロジェクト側の allow/deny リストだけを整えても、確認ダイアログに頼った設計だと素通りする。docs/destructive-ops.md にはこの前提をそのまま書いてある。
グローバル
~/.claude/settings.jsonはBash(*)を許可しskipAutoPermissionPrompt=true。確認プロンプトは自動スキップされるため、deny リスト + fail-closed な PreToolUse フックだけが実効防壁。
つまり「対話で人間が止める」を前提にした設計は最初から成立しない。止めるなら構造で止めるしかない。
deny リストとフックの二枚構え
そこで用意したのが二段構えだ。1段目は .claude/settings.json の permissions.deny。wrangler deploy や stripe、gh release のようなコマンドパターンを列挙してある。npx wrangler deploy や pnpm dlx wrangler deploy、pnpm run deploy のようなラッパー経由の呼び出しも、実際にはここに個別に列挙して塞いである。ただし deny リストは列挙式なので、書き漏らした言い回しは素通りする。たとえば yarn run deploy は deny には載っていない。
2段目が scripts/hooks/no_destructive_ops.py。PreToolUse フックとして、Bash・WebFetch・Write・playwright の一部ツールが呼ばれるたびに割り込み、コマンド文字列を正規表現で検査する。deny リストが「既知のツール名の列挙」で止めるのに対し、フックは「コマンド文字列のパターン」で止めるので、deny に書き漏らした yarn run deploy のようなケースもここで拾える。実際、BLOCKED_CMD には次のような行がある。
(r"\b(pnpm|npm|yarn)\s+(run\s+)?(deploy|publish|release)\b", "deploy/publish script"),
deny リスト単体でもフック単体でもなく、両方を重ねているのは、片方が抜けたときにもう片方が拾う設計にしたかったからだ。
解釈できないものは通さない
フックの中身で一番効いているのは、個々のパターンよりも「わからなければ拒否する」という基本姿勢だと思う。main() の入り口で、標準入力が空なら即座に deny する。
if not raw.strip():
deny("破壊的操作ゲート: 入力が空(matcher一致ツールで検証不能)。既定で拒否(fail-closed)。")
return
JSON のパースに失敗したときも、ツール判定中に例外が出たときも同じで、全部 deny に倒れる。docs/destructive-ops.md にも「sidework の legality_gate は fail-open だったが、本ハブは自動スキップ環境のため fail-closed に反転している」と記した通り、以前触っていた別プロジェクトでは「判定できないなら通す」設計だったのを、今回は逆にした。理由は単純で、確認プロンプトが自動スキップされる環境では、フックが唯一の防壁だから、フック自体が迷ったときに通してしまうと防壁の意味がなくなる。
自分の説明文で自分を止めた話
fail-closedにすれば安心だ、としばらく思っていた。でも作っている途中、地味に困った誤検知に当たって、そう単純でもないと知った。フックの正規表現は、Bash に渡ってくるコマンド文字列全体にマッチをかけている。構造化された引数を見ているわけではないので、git commit -m "..." のコミットメッセージの中に wrangler deploy という文字列が入っているだけで、実際にはデプロイしていないのに check_bash が反応してしまう。
このガードレール自体をドキュメント化する作業で、コミットメッセージに deny 対象のコマンド名をそのまま書いたら、その git commit 自体が止まった。フックからすれば区別のしようがない。文字列としては同じだからだ。
これは仕様として直すべきバグというより、「全文字列マッチで作る以上、避けられないトレードオフ」だと理解した方が早い。実際の付き合い方は地味で、コミットメッセージで deny 対象のコマンド名に触れるときは、英語のコマンド名をそのまま書かずに「wrangler の deploy 系」のように分けて書くか、カタカナで「デプロイ系コマンド」と表現する。正規表現は ASCII の deploy という並びを見ているので、カタカナ表記なら引っかからない。厳密さより実用性を取った、という程度の話だが、fail-closed で作ると自分自身の説明文にまで反応するというのは、作ってみるまで実感が薄かった。
3点同期と、外した実績
ガードレールの根拠は docs/destructive-ops.md を正本にしていて、変更するときは必ず「docs/destructive-ops.md → フックの BLOCKED_* → settings.json の deny」の順で3ファイルを同期する、というルールにしてある。1箇所だけ直すと、正本とコードがズレて、次に見たときにどちらが正しいか分からなくなるからだ。
このゲートは絶対に外さないという運用ではなく、人間の明示指示があれば外せる。実際、2026-07-20 に git push と gh repo create / gh pr create を deny から外している。docs/destructive-ops.md の解除ログにはこう残した。
2026-07-20:
git pushとgh repo create/gh pr createを許可に変更(CK の明示指示「プライベートリポジトリ作成してよいです、ガードレール外して」)。対象はプライベートリポジトリ運用。デプロイ/公開/Stripe/DNS/release/merge のゲートは不変。
外すこと自体は禁止していない。ただし、口頭の指示だけで静かに外すのではなく、正本ファイルに解除理由と対象範囲を書き残す、というのを条件にしている。あとから見返したときに「なぜここだけ空いているのか」が分かるようにしておかないと、ガードレールの意味が薄れる。
破壊的操作ゲートの話は以上。実際に手を動かしたゲームの方はKludge Worksから触れる。