◉CLAUDE LABEN
●THREADS — Messages API に message threads がベータで登場しました(11/2)。会話をサーバーに保持し、以降は新しいメッセージだけを送ります●CONSOLE — Console の IP 許可リストがベータで登場しました(11/5)。許可外のアドレスからのリクエストは拒否されます●SONNET 4.5 — Claude API での提供終了は 11/30、残り25日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST — Opus 4.8 の fast mode は非推奨になり、11/30 に除去されます。移行先は Opus 5.5●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●THREADS — Messages API に message threads がベータで登場しました(11/2)。会話をサーバーに保持し、以降は新しいメッセージだけを送ります●CONSOLE — Console の IP 許可リストがベータで登場しました(11/5)。許可外のアドレスからのリクエストは拒否されます●SONNET 4.5 — Claude API での提供終了は 11/30、残り25日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST — Opus 4.8 の fast mode は非推奨になり、11/30 に除去されます。移行先は Opus 5.5●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-08-07上級

権限モードを変えても止まらない書き込み — サブエージェント用 PreToolUse ゲートを150通りで測る

ワークフローのサブエージェントによるファイル編集は、セッションの権限モードに関わらず自動承認されます。残る境界である agent_id / agent_type ベースの PreToolUse フックを150通りのペイロードで測り、素通りしていた54件の内訳と、フェイルクローズへ寄せた実装を共有します。

Claude Code281PreToolUse5サブエージェント17権限設計7

✦ プレミアム記事

自動運用の設定を見直していて、手が止まった箇所があります。

「ワークフローのサブエージェントは、セッションの権限モードに関わらずファイル編集を自動承認する」。この一行を読んだとき、自分の環境で何が起きているのかを説明できませんでした。個人開発でブログの生成パイプラインを回している以上、書き込み先の境界は自分で引いているつもりでいたのですが、その境界が権限モードの側にしか置かれていなかったのです。

シェルコマンドや許可リスト外の MCP 呼び出しは実行中でも確認を挟みます。ファイル書き込みだけが挟みません。auto / bypass / claude -p では、起動時のプロンプト自体が省かれます。

では何が残るのか。公式ドキュメントを追うと、PreToolUse フックの入力にはサブエージェント内でのみ agent_id と agent_type が届くと書かれています。つまり「誰の書き込みか」をフック側で見分けられます。ここが最後の境界になります。

そう理解して、ありがちな形でゲートを書き、150通りのペイロードを流してみました。結果は想定と逆でした。

素通りは54件でした — まず母集合を作って測る

判定の質を語る前に、判定を通す入力の幅を決める必要があります。PreToolUse の共通入力フィールドから、実際に分岐に効く軸だけを取りました。

  • permission_mode — default / plan / acceptEdits / auto / dontAsk / bypassPermissions の6値(UI で Manual と表示されるモードは default として届きます)
  • 呼び出し元 — メインスレッド、名前付きサブエージェント2種、プラグイン配布の my-plugin:reviewer、--agent 起動(agent_type はあるが agent_id はない)の5通り
  • ツールと書き込み先 — 許可ルート内、許可ルート外、テスト配下、CI 設定への書き込み、そして file_path を欠いた入力の5通り

6 × 5 × 5 で150通り。この母集合を組む Python が以下です。

# payloads.py — PreToolUse ペイロードの母集合
import copy
import itertools
 
BASE = {
    "session_id": "sess_gate_probe",
    "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
    "transcript_path": "/home/u/.claude/projects/x/transcript.jsonl",
    "cwd": "/repo",
    "hook_event_name": "PreToolUse",
    "tool_use_id": "toolu_probe",
}
 
MODES = ["default", "plan", "acceptEdits", "auto", "dontAsk", "bypassPermissions"]
 
ORIGINS = [
    {},                                                # メインスレッド
    {"agent_id": "ag_01", "agent_type": "doc-writer"},
    {"agent_id": "ag_02", "agent_type": "test-runner"},
    {"agent_id": "ag_03", "agent_type": "my-plugin:reviewer"},
    {"agent_type": "doc-writer"},                      # --agent 起動(agent_id なし)
]
 
TOOLS = [
    ("Write", {"file_path": "/repo/content/a.mdx", "content": "x"}),
    ("Write", {"file_path": "/repo/src/config/pricing.ts", "content": "x"}),
    ("Edit",  {"file_path": "/repo/tests/t.spec.ts", "old_string": "a", "new_string": "b"}),
    ("Edit",  {"file_path": "/repo/.github/workflows/deploy.yml",
               "old_string": "a", "new_string": "b"}),
    ("Write", {"content": "x"}),                       # file_path 欠落
]
 
 
def build():
    out = []
    for mode, origin, (tool, tin) in itertools.product(MODES, ORIGINS, TOOLS):
        p = copy.deepcopy(BASE)
        p["permission_mode"] = mode
        p.update(origin)
        p["tool_name"] = tool
        p["tool_input"] = tin
        out.append(p)
    return out

流す相手は、私が最初に書いたゲートです。素直に書くとこうなります。

# naive_gate.py — サブエージェントの書き込みだけを制限したい、という最初の実装
import json
import sys
 
ALLOWED_ROOTS = {
    "doc-writer": ["/repo/content"],
    "test-runner": ["/repo/tests"],
}
 
data = json.load(sys.stdin)
agent_type = data["agent_type"]
path = data["tool_input"]["file_path"]
 
roots = ALLOWED_ROOTS.get(agent_type, [])
if not any(path.startswith(r) for r in roots):
    print(f"{agent_type} は {path} へ書き込めません", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

150通りを流した結果です。

判定件数
BLOCK(exit 2 でツール呼び出しを中止)78
PASS(無言で許可)18
PASS(フック異常のまま続行)54

150件中54件、率にして36%が「フックが落ちたのに書き込みは進む」状態でした。ブロックしたつもりの箇所ではありません。フックが例外で終わった箇所です。

exit 1 は失敗ではなく「意見なし」として扱われる

原因は Claude Code の終了コードの規約にあります。

ほとんどのフックイベントで、動作を止めるのは exit 2 だけです。exit 1 は non-blocking error として扱われ、Unix の慣習では失敗を意味する終了コードであるにもかかわらず、動作はそのまま進みます。例外は WorktreeCreate で、ここだけは非ゼロなら中止されます。

Python スクリプトが未捕捉の例外で終われば、終了コードは 1 です。つまり KeyError ひとつで、ゲートは「止める」から「意見を言わない」へ静かに切り替わります。

素通り54件の内訳を取ると、性質が二つに分かれました。

呼び出し元原因件数
メインスレッドagent_type 欠落24
メインスレッドfile_path 欠落6
サブエージェントfile_path 欠落24

メインスレッド由来の30件は、結果として通ってよいものです。agent_id はサブエージェント内でのみ届く任意フィールドなので、存在しないのは正常です。ただし通したのは設計ではなく、例外が偶然そう振る舞っただけでした。

問題は下段の24件です。サブエージェントからの書き込みでありながら file_path が取れず、ゲートは何の判断も返さないまま、書き込みは進みました。ここを見つけた瞬間、境界を引いたつもりでいた自分の設定が、実は「例外が出なかった経路にだけ効く境界」だったと分かりました。

事前の予想は逆でした。厳しすぎて開発が止まる方を心配していたのに、実際は最も情報の足りない入力ほど素通りしていたのです。ゲートは、判定できないときにこそ止まらなければ意味がありません。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦PreToolUse フックが exit 1 で終わると書き込みは通ってしまう。150通りのペイロードで素通りが何件出るかを実測した内訳が手に入る
✦agent_type ごとに書き込みルートを限定する完全な PreToolUse ゲート実装と、settings.json への配線がそのまま持ち帰れる
✦接頭辞一致で許可ルートを判定すると /repo/content の許可が /repo/content-archive まで通る。その再現と修正が確認できる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ Claude Code2026-10-15
日本語フォルダ名を守るはずのフックが、一度も止めてくれませんでした — Python の stdin が cp932 で読んでいた話
「経理」「議事録」などの日本語フォルダを守る PreToolUse フックが、Python の stdin 既定が cp932 の環境では素通りします。再現の手順と 3 行の直し方、確認用の 1 コマンドを書き残します。
⟐ Claude Code2026-10-14
onFailure を block にしても、jq が無い環境では公式例の rm -rf ブロックは素通りしました
PreToolUse の門番フックは、落ちたときに通すのか止めるのか。公式例を jq の無い環境で走らせると exit 0 で素通りしました。終了コード10通りの実測、直し方、settings.json を走査して素通りを洗い出すスクリプトをまとめます。
⟐ Claude Code2026-10-05
「コミットは私がします」と書いたのに git log に一行増えていた朝、指示を強めるのをやめました
Claude Code に「コミットしないで」と頼んだのにコミットされたとき、まず git reset --soft で戻し、permissions.deny と PreToolUse hook の二層で物理的に止める手順です。deny ルールが素通しする git -c の形と、hook の試し方を書き残します。