CLAUDE LABEN
RUNNER — claude self-hosted-runner で、自前のマシンやコンテナを web・モバイル・デスクトップセッションの実行先にできるようになりました(Team / Enterprise)PLUGIN — HTTPS 経由の zip からプラグインを導入できます。git も npm も不要で、任意で SHA-256 のピン留めが可能ですBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX により、AWS_REGION 由来ではない特定のクロスリージョン推論プロファイルを優先できますSTATUS — サンドボックスや MCP 入力の応答待ちセッションが、Working ではなく Needs input と表示されるようになりましたDLP — Enterprise の Inference Hooks がベータで提供中です。プロンプトはモデルに届く前に組織のサーバへ回り、通常5秒以内の allow / deny 判定を待ちますWORKBENCH — 旧 Workbench と実験的なプロンプトツール API の廃止は8月17日、Sonnet 5 のプロモ価格は8月31日までですRUNNER — claude self-hosted-runner で、自前のマシンやコンテナを web・モバイル・デスクトップセッションの実行先にできるようになりました(Team / Enterprise)PLUGIN — HTTPS 経由の zip からプラグインを導入できます。git も npm も不要で、任意で SHA-256 のピン留めが可能ですBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX により、AWS_REGION 由来ではない特定のクロスリージョン推論プロファイルを優先できますSTATUS — サンドボックスや MCP 入力の応答待ちセッションが、Working ではなく Needs input と表示されるようになりましたDLP — Enterprise の Inference Hooks がベータで提供中です。プロンプトはモデルに届く前に組織のサーバへ回り、通常5秒以内の allow / deny 判定を待ちますWORKBENCH — 旧 Workbench と実験的なプロンプトツール API の廃止は8月17日、Sonnet 5 のプロモ価格は8月31日までです
記事一覧/Claude Code
Claude Code/2026-08-07上級

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

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

Claude Code213PreToolUse2サブエージェント13権限設計2

プレミアム記事

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

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

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

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

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

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

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

  • permission_modedefault / 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-07-27
許可リストに書いた環境変数は誰の環境で展開されるのか — Managed MCP の実効ポリシーを指紋で確かめる
Managed MCP の allowlist・denylist に書いた環境変数参照の解決元が、起動時環境と managed-settings の env に変わりました。同じ設定ファイルが環境ごとに別のポリシーになる仕組みを手元で再現し、実効ポリシーを指紋で突き合わせる起動前チェックを組みました。
Claude Code2026-07-18
/fork と /subtask の使い分け — 分岐は手元から切り離し、委譲はセッションに残す
Claude Code 2.1.212 で /fork は会話をバックグラウンドセッションへ複製する挙動に変わり、従来のセッション内サブエージェントは /subtask へ移りました。2つの使い分けと、分岐を増やす前に決めておきたい上限の話です。
Claude Code2026-07-17
夜間に回した並列サブエージェントの記録が、追えなくなった — --forward-subagent-text で増えたログを絞り直す
サブエージェントのテキストと思考を stream-json へ流せるようになった一方で、無人実行のログ量は跳ね上がります。実測値をもとに、事後解析に効く記録だけを残す3層の保存方針と相関IDの設計をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →