CLAUDE LABEN
APPLY — ant CLI v1.30.0 に ant apply が入りました。エージェント・環境・スキル・メモリストア・デプロイメントを、リポジトリのファイルから作成・更新できますLOCKFILE — 書き出される claude-lock.json は必ずコミットしてください。忘れると、実行のたびに同じリソースが新しく作られていきますPLAN — ant apply は適用の前に計画を表示し、承認を挟みます。CI で無人実行するなら、この承認をどう扱うかを先に決めておくことですPRICING — Claude Sonnet 5 の 3 ドル / 15 ドルへの値上げは実施されていません。導入価格の 2 ドル / 10 ドルがそのまま標準価格になりましたSOURCE — 価格と版番号は二次ニュースに誤りが混じります。platform.claude.com のリリースノートで裏を取ってから書くようにしていますBETA — Agent Skills と Skills API はベータを抜け、skills-2025-10-02 ヘッダが不要になりました。Files API はヘッダの有無でレスポンス形式が変わる点に注意ですAPPLY — ant CLI v1.30.0 に ant apply が入りました。エージェント・環境・スキル・メモリストア・デプロイメントを、リポジトリのファイルから作成・更新できますLOCKFILE — 書き出される claude-lock.json は必ずコミットしてください。忘れると、実行のたびに同じリソースが新しく作られていきますPLAN — ant apply は適用の前に計画を表示し、承認を挟みます。CI で無人実行するなら、この承認をどう扱うかを先に決めておくことですPRICING — Claude Sonnet 5 の 3 ドル / 15 ドルへの値上げは実施されていません。導入価格の 2 ドル / 10 ドルがそのまま標準価格になりましたSOURCE — 価格と版番号は二次ニュースに誤りが混じります。platform.claude.com のリリースノートで裏を取ってから書くようにしていますBETA — Agent Skills と Skills API はベータを抜け、skills-2025-10-02 ヘッダが不要になりました。Files API はヘッダの有無でレスポンス形式が変わる点に注意です
記事一覧/Claude Code
Claude Code/2026-08-07上級

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

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

Claude Code250PreToolUse2サブエージェント15権限設計4

プレミアム記事

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

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

シェルコマンドや許可リスト外の 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/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

関連記事

Claude Code2026-09-04
Claude Code に project.pbxproj だけは直接触らせていません
Xcode のプロジェクトファイルは、開けなくなる壊れ方より開けてしまう壊れ方のほうが高くつきました。直接編集をやめてスクリプト経由に寄せるまでに測ったことと、いま引いている線を書き残します。
Claude Code2026-08-17
フォーク既定のサブエージェントで、レビュー役だけは名前で呼ぶという線引き
サブエージェントのフォークが既定になり、親の会話をそのまま引き継ぐようになりました。作業は継承させ、判定は継承させない。その境界をリポジトリに宣言し、ずれを機械的に見つけるまでの設計です。
Claude Code2026-08-16
Todo ツールが既定オフになった Claude Code で、多段作業の進捗をどう追うか
Claude Code v2.1.233 以降、新しいモデルでは TodoWrite と Task 系ツールが既定で読み込まれません。環境変数で戻す前に確かめるべき3つの分岐と、チェックリストの代わりに置いた作業台帳の実装、並列実行で台帳が壊れた境界の実測結果をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます