【Claude Code 連載 第80回】Agent SDK の権限設定(agent-sdk/permissions)

スポンサーリンク
【Claude Code 連載 第80回】Agent SDK の権限設定(agent-sdk/permissions) 用語解説
【Claude Code 連載 第80回】Agent SDK の権限設定(agent-sdk/permissions)
この記事は約23分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

スポンサーリンク

背景

この連載では、Claude Codeの公式ドキュメントを1ページずつ読み解いていきます。公式ドキュメントは情報が網羅されている分、「結局どの機能を、どんな場面で使えばいいのか」は自分で考える必要があり、読むのに意外と時間がかかります。そこで、私が実務で使うために読み込んだ内容を「使う場面→実例コード」の順に整理して残していくことにしました。専門家の解説というより、一次情報を読んだ記録です。推測や動作を確認していない部分には、その都度そう書きます。

公式ドキュメント:https://code.claude.com/docs/ja/agent-sdk/permissions

1. 一言でいうと

Agent SDK でツール呼び出しを通すか止めるかは、6段階の評価順序(フック → 拒否ルール → 確認ルール → 権限モード → 許可ルール → canUseTool)で決まる。このページは、その順序の中で「許可/拒否ルール」と「権限モード」がどう効くかを定めたものです。

Claude Code 本体の権限(第16回)と語彙はほぼ同じです。ただし SDK には、対話の承認ダイアログの代わりに canUseTool というコールバックがあります。この回の核心は、どの設定をするとそのコールバックが素通りされるかにあります。


2. どういう場面で役立つか

シーン1:読み取り専用の調査エージェントを CI に置く

コードベースを読んでレポートを書くだけのエージェントを、人がいない環境で動かすケースです。allowedTools で読み取り系ツールを並べ、permissionMode: "dontAsk" と組み合わせます。これで、リストに無い操作は確認を待たずに拒否されるロックダウン構成になります。canUseTool を書き忘れても拒否に倒れるので、暗黙の挙動に頼らずに済みます。

シーン2:最初は慎重に、方針が見えたら編集を任せる

リファクタリングの依頼を default モードで始め、Claude の最初の方針を確認します。問題なければセッション途中で acceptEdits に切り替えます。モードはストリーミング中に動的に変えられ、次のツールリクエストから即座に効くので、段階的に信頼を広げる運用がコードで書けます。

シーン3:自前の承認 UI を持つアプリを作る

自社アプリの画面で「このコマンドを実行してよいか」をユーザーに聞く場合は、canUseTool に承認処理を実装します。このとき、allowedTools に裸のツール名を入れたり bypassPermissions を使ったりすると、承認処理がそのツールについて黙って呼ばれなくなります。どの設定がコールバックを迂回させるかを知らないと、承認 UI が機能していないことに気づけません。

不要・向かないケース

  • Claude Code を対話で使うだけの人:このページは SDK の options の話です。CLI の権限は第16回(permissions / permission-modes)の範囲です。
  • 「全ツール呼び出しで必ず走らせたいチェック」:これを canUseTool に書くのは向いていません。自動承認で素通りされる経路があるからです。公式は、この用途には PreToolUse フックを使うよう明示しています(後述)。
  • bypassPermissions を使いつつ allowedTools で絞るつもりの構成:これは成立しません。絞りたいなら disallowedTools を使います(後述)。

3. コードと仕組みの解説

原文の fenced コードブロックは5本です(TypeScript 単独1本、Python/TypeScript の対訳2組)。全数を改変なしで引用します。対訳は、読者がどちらかの言語を使う前提で両方載せています。ページの本体は評価順序の説明と2つの表なので、それらも保持したうえで解説を加えます。

3-0. バージョン要件(機能ごとに分散)

挙動必要バージョン
MCP ツールの _meta["anthropic/requiresUserInteraction"] アノテーションClaude Code v2.1.199 以降
plan モードで、ファイルを変更するシェルコマンド(touch・rm など)も canUseTool に回すv2.1.212 以降
auto モードで、重要なパスの削除を分類器へ回すv2.1.218 以降
TypeScript の permissionPrompts: 'none'v2.1.259 以降
サブエージェントの bypassPermissions 例外(後述)v2.1.267 以降

3-1. 評価順序(このページの土台)

ツールがリクエストされると、SDK は次の順で判定します。

順ステップ何が起きるか
1フック拒否すれば終わり。allow を返しても、後続の deny/ask ルールはスキップされない
2拒否(deny)ルールdisallowed_tools と settings.json 由来。マッチすれば bypassPermissions でもブロック
3確認(ask)ルールsettings.json 由来。マッチすれば bypassPermissions でも canUseTool へ
4権限モードbypassPermissions はここで承認。acceptEdits はファイル操作を承認。plan は書き込みを canUseTool へ送る
5許可(allow)ルールallowed_tools と settings.json 由来。ツール自身が承認する呼び出しもここで決まる
6canUseToolここまでで決まらなかったものだけが来る。dontAsk ではスキップされ拒否

押さえておくべき点は次の5つです。

(a) フックの allow は「通行証」ではない。 フックが許可しても、deny ルールと ask ルールは評価されます。さらに PreToolUse フックの allow では、重要なパス(critical paths)を対象とする rm・rmdir の削除を承認できません。フックは止める力は強いが、通す力は限定的という非対称になっています。

(b) 裸名の deny はツールそのものを消す。 Bash のようにツール名だけの deny ルールは、評価が始まる前に Claude のコンテキストからツールを削除します。そのため、ステップ2で実際にチェックされるのは Bash(rm *) のようなスコープ付きルールだけです。

(c) ask ルールは bypass に勝つ。 ask ルールにマッチした呼び出しは、bypassPermissions でも確認のために canUseTool へ流れます。同じ扱いを受けるものが、ほかに3種類あります。

  • AskUserQuestion
  • _meta["anthropic/requiresUserInteraction"] を設定した MCP ツール
  • 組織が ask に設定した claude.ai コネクタのツール

これらは allow ルールにマッチしてもコールバックへ流れます。コネクタの場合、コールバックが受け取る理由は Your organization requires approval for this tool です。dontAsk モードではプロンプトを出せないので、いずれも拒否になります。

(d) 権限モードは許可ルールより前に評価される。 図にすると「モード → allow」の順です。bypassPermissions はモードの段階で全部を承認するので、allow ルールまで判定が回りません。これが後述の「allowed_tools は bypass を制約しない」の構造的な理由です。第79回で「allowedTools は許可リストではなく事前承認リスト」と整理しましたが、その裏付けがこの順序です。

(e) ステップ5には「ルール不要で通るもの」がある。 作業ディレクトリ内のファイル読み取りや読み取り専用の Bash コマンドは、ツール自身が承認するのでルールがなくても通ります。一方、重要なパスへの rm・rmdir は allow ルールでも決して承認されません。行き先はモードによって3通りに分かれます。

  • プロンプトを出すモード:コールバックへ
  • auto モード(v2.1.218 以降):分類器へ
  • dontAsk モード:拒否

ステップ6の TypeScript 専用オプション。 permissionPrompts: 'none' を設定すると canUseTool は呼ばれません。代わりに PermissionRequest フックが判断する機会を持ち、フックも判断しなければ拒否されます。第75回で扱った -p の --permission-prompts none と対になる SDK 側の口と読めます(筆者の対応づけ。原文は両者の関係を述べていません)。

3-2. コールバックが素通りされる警告:CLAUDE_SDK_CAN_USE_TOOL_SHADOWED

TypeScript SDK では、canUseTool を渡しつつ、コールバックより前に自動承認する設定をしていると、クエリ構築時に Node.js のプロセス警告が1回出ます。警告コードは CLAUDE_SDK_CAN_USE_TOOL_SHADOWED です。

設定警告が出るか
permissionMode: 'bypassPermissions'出る
"Read" のような裸の allowedTools エントリ出る(エントリごとに対象ツール全体を先に承認するため)
Bash(ls *) のような指定子付きエントリ出ない
acceptEdits モード出ない
設定ファイル由来の allow ルールチェック対象外(警告には現れない)

最後の行に注意が要ります。settings.json で許可したツールについては、コールバックが素通りされていても警告されません。警告が出ないことは、承認処理が全ツールで動いている証明にはなりません。

警告は process.on('warning', ...) で受け取り、コードでマッチさせてログに出すか抑制します。モードやルールに関係なく全ツール呼び出しをゲートしたいなら、公式の答えは PreToolUse フックです。

3-3. 許可ルールと拒否ルールの書き方

allowed_tools / disallowed_tools(TypeScript では allowedTools / disallowedTools)は、評価フロー内の allow・deny リストにエントリを足すものです。原文の表を保持します。

オプション効果
allowed_tools=["Read", "Grep"]Read と Grep は自動承認。リストに無いツールも存在し続け、承認が必要な呼び出しは権限モードと canUseTool へフォールスルー
disallowed_tools=["Bash"]Bash のツール定義がリクエストから削除される。Claude はツールを認識せず、実行を試みることもできない
disallowed_tools=["Bash(rm *)"]Bash は使えるまま。rm * にマッチする呼び出しは bypassPermissions を含む全モードで拒否。/bin/rm を含む他の呼び出しはモードへフォールスルー
disallowed_tools=["*"]全ツール定義を削除。deny ではツール名グロブが使え、"*" は全ツール、"mcp__*" は全サーバーの全 MCP ツールにマッチ

表の3行目は読み飛ばしやすい点です。Bash(rm *) を拒否しても、/bin/rm のような書き方は同じ形でマッチしないと原文自身が書いています。スコープ付き deny は文字列パターンなので、迂回の余地が残ります。確実に塞ぎたいなら、ツールごと消すかフックで判定する、というのがこの表から読み取れる実務上の含意です(筆者の評価)。

また、allowed_tools にタスク追跡ツールのどれかを名前で入れると、それだけでセッションがタスク追跡にオプトインします。許可リストに書いたことが機能の有効化を兼ねる例外的な挙動です。

allow と deny でグロブの扱いが違う。 deny では "*" や "mcp__*" が使えます。allow では、グロブはリテラル mcp__<server>__ の後ろにしか書けず、サーバー名の部分にはグロブを使えません。

  • mcp__puppeteer__*:puppeteer サーバーの全ツールにマッチする
  • mcp__github__get_*:github サーバーの get_ 系ツールにマッチする
  • allowed_tools=["*"] や ["mcp__*"]:起動時に警告が出て無視され、何も自動承認しない

「全部許可」を書いたつもりで何も許可されていない、という結果になります。これは安全側に倒した設計です。

パス付きルールの落とし穴は2つあります。

1つ目は、Edit(path) ルールが Write と NotebookEdit を含むファイルを書く全組み込みツールを管理する点です。逆に Write(path) ルールはファイル権限チェックでマッチしません。書き込みを縛るつもりで Write(...) と書くと、効いていないことになります。

2つ目はパスのアンカーです。

書き方意味(SDK の options で渡した場合)
Edit(//secrets/**)ディスク上の絶対パス /secrets 以下
Edit(/secrets/**)ルールのソース基準。SDK の options 由来ではセッションの作業ディレクトリ基準になる

単一スラッシュで書いた Edit(/secrets/**) の deny は、ディスク上の /secrets をブロックしません。絶対パスのつもりで書くと、防御が空振りします。アンカー形式は4種類あり、設定ファイル側での解決方法は Read と Edit ルールの公式ページに回されています。

3-4. 自動承認は canUseTool に到達しない

原文の警告ボックスの内容です。acceptEdits・bypassPermissions・allow ルールのどれかで先に承認された呼び出しは canUseTool を通りません。そこに書いた権限チェックは、そのツールについて静かにバイパスされます。

例外として、許可ルールがマッチしてもコールバックに届くものが4つあります。

  • AskUserQuestion
  • requiresUserInteraction を付けた MCP ツール
  • 組織が ask に設定したコネクタツール
  • 重要なパスへの rm・rmdir

モードによる違いもあります。auto モードでは重要パスの削除だけが分類器へ行き(v2.1.218 以降)、他の3つはコールバックへ届きます。dontAsk モードでは4つとも拒否され、コールバックは呼ばれません。

カバー範囲はエントリの形で変わります。

  • Read や mcp__github__get_issue のような裸名:そのツールの全呼び出しを自動承認する
  • Bash(npm test *) のようなスコープ付き:マッチしたものだけを承認し、他の Bash 呼び出しはコールバックへ落ちる

「毎回必ず走らせたいチェック」は PreToolUse フックに置きます。フックは全ステップより先に走り、フックの拒否は bypassPermissions でも効きます。第72回の「ガードレールはフックに入れる」という原則が、SDK でも同じ形で成り立っています。

3-5. ロックダウン構成(コード1本目)

const options = {
  allowedTools: ["Read", "Glob", "Grep"],
  permissionMode: "dontAsk"
};

この構成で起きることは次の3つです。

  1. リストした Read・Glob・Grep は承認される(どのモードも自動承認しないアクションは除く)。
  2. プロンプトが出るはずだった他の呼び出しは、すべて拒否される。
  3. default モードで承認不要な呼び出しは、リストに無くても実行される。

3つ目は見落としやすい点です。例としては読み取り専用 Bash コマンド、Agent のように実行前に尋ねないツール、作業ディレクトリ内のファイル読み取りが挙げられています。つまり「3ツールしか使えないエージェント」にはなりません。Agent(サブエージェント起動)や読み取り専用 Bash は通るので、ツールを Claude の到達範囲から完全に外したいなら、裸名を disallowedTools に足す必要があります。

3-6. allowed_tools は bypassPermissions を制約しない

3-1 の (d) の帰結です。allowed_tools=["Read"] と permission_mode="bypassPermissions" を同時に設定しても、「Read だけ許可」にはなりません。Bash・Write・Edit を含む全ツールが承認されます。リストに無いツールはモードの段階へ落ち、そこで bypass が承認するからです。bypass を使いつつ特定ツールを止めたいなら、disallowed_tools を使います。

3-7. settings.json のルールが読まれる条件

allow・deny・ask の3種のルールは .claude/settings.json にも宣言的に書けます。読み込まれるのは project 設定ソースが有効なときで、既定の query() オプションでは有効です。ただし setting_sources(TS:settingSources)を明示的に設定した場合は、"project" を含めないとこれらのルールは適用されません。

第79回で、settingSources: [] にすればファイルシステム設定を全部切れると扱いました。その裏返しとして、分離のために settingSources を絞ると、プロジェクトの deny ルールも一緒に消えることになります。防御を settings.json に頼っている場合、options 側で deny を明示し直す必要がある、というのが筆者の読みです。

3-8. 権限モード6種

モード説明ツール動作
default標準の権限動作モードによる自動承認なし。承認が要り allow にマッチしない呼び出しは canUseTool へ
dontAskプロンプトの代わりに拒否プロンプトになるはずの呼び出しは拒否。事前承認済みと承認不要なものは実行。コネクタ(ask)・ユーザー対話必須ツール・重要パス削除は事前承認していても拒否。canUseTool は呼ばれない
acceptEditsファイル編集を自動承認ファイル編集と mkdir・rm・mv などのファイルシステム操作を自動承認
bypassPermissions権限チェックをバイパスどのモードも自動承認しないアクションを除き、プロンプトなしで実行
plan計画モードソースを編集せず探索と計画を行う。編集は自動承認されず canUseTool へ
autoモデル分類による承認分類器が承認・拒否を判断する。利用可能性は自動モードの公式ページ参照

3-9. サブエージェントの権限モード継承

サブエージェントは、原則として親セッションの権限モードで動きます。例外は、AgentDefinition に permissionMode を設定し、かつ親が default・dontAsk・plan のいずれかにある場合です。この場合はサブエージェント側の設定が使われます。

ただし、その例外でも "bypassPermissions" という値は適用されません。サブエージェントが bypass で動くのは、親自身が bypass のときだけです(この例外は v2.1.267 以降)。公式は理由も書いています。サブエージェントはメインと別のシステムプロンプトを持つ可能性があり、挙動の制約が弱いので、bypass を継承すると完全に自律的なシステムアクセスを与えることになる、というものです。

整理すると次のようになります。親が慎重なモードなら、サブエージェントを個別に緩めることはできる。ただし bypass まで緩めることはできない。 親が acceptEdits や bypassPermissions のときは、サブエージェントは親と同じモードで動きます。第67回の Claude Code ワークフロー(内部のサブエージェントは常に acceptEdits)とは別の仕組みなので、混同しないでください。

3-10. モードの設定方法(コード2〜5本目)

クエリ時に設定する場合。 permission_mode(Python)/ permissionMode(TypeScript)を渡します。動的に変えない限り、セッション全体に適用されます。

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions


async def main():
    async for message in query(
        prompt="Help me refactor this code",
        options=ClaudeAgentOptions(
            permission_mode="default",  # Set the mode here
        ),
    ):
        if hasattr(message, "result"):
            print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

async function main() {
  for await (const message of query({
    prompt: "Help me refactor this code",
    options: {
      permissionMode: "default" // Set the mode here
    }
  })) {
    if ("result" in message) {
      console.log(message.result);
    }
  }
}

main();

結果の判定方法が言語で違います。Python は hasattr(message, "result")、TypeScript は "result" in message を使っています。

ストリーミング中に変更する場合。 set_permission_mode()(Python)/ setPermissionMode()(TypeScript)を呼ぶと、以降のツールリクエストに即座に効きます。

import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions


async def main():
    async with ClaudeSDKClient(
        options=ClaudeAgentOptions(
            permission_mode="default",  # Start in default mode
        )
    ) as client:
        await client.query("Help me refactor this code")

        # Change mode dynamically mid-session
        await client.set_permission_mode("acceptEdits")

        # Process messages with the new permission mode
        async for message in client.receive_response():
            if hasattr(message, "result"):
                print(message.result)


asyncio.run(main())
import { query } from "@anthropic-ai/claude-agent-sdk";

async function main() {
  const q = query({
    prompt: "Help me refactor this code",
    options: {
      permissionMode: "default" // Start in default mode
    }
  });

  // Change mode dynamically mid-session
  await q.setPermissionMode("acceptEdits");

  // Process messages with the new permission mode
  for await (const message of q) {
    if ("result" in message) {
      console.log(message.result);
    }
  }
}

main();

言語による違いは第79回で指摘したとおりです。Python は query() 関数ではなく ClaudeSDKClient が必要で、async with で開いたクライアントに対して query() → set_permission_mode() → receive_response() の順に呼びます。TypeScript は query() の戻り値 q 自体がモード変更メソッドを持ち、for await でそのまま反復します。

3-11. 各モードの細部

acceptEdits。 自動承認されるのはファイル編集(Edit・Write)と、7つのファイルシステムコマンド(mkdir・touch・rm・rmdir・mv・cp・sed)です。どちらも作業ディレクトリか additionalDirectories 内のパスに限られます。次の3つは自動承認されません。

  • スコープ外のパスでの作業
  • 保護されたパスへの書き込み
  • 重要なパスへの rm・rmdir

「ファイルシステム操作ではない Bash コマンド」は通常の権限が必要です。rm が自動承認の側に入っている点は意識しておくべきです。

dontAsk。 事前承認の経路として、allowed_tools・settings.json の allow ルールに加えてフックによる承認も挙げられています。ただし PreToolUse フックの allow では、重要パスの削除は通りません。公式は使いどころを、ヘッドレスエージェントで固定のツール面を定め、「canUseTool が無いこと」への暗黙の依存よりハード拒否を選ぶ場合、と書いています。第77回で扱ったとおり、default は canUseTool が無いと拒否に倒れます。結果は似ていても、意図を明示するのが dontAsk という位置づけです。

bypassPermissions。 フックは引き続き走り、操作を止められます。Linux と macOS では、root として、または認識されたサンドボックスの外で sudo 下で起動すると、Claude Code は起動を拒否し、クエリは最初のターンの前に失敗します。第77回で見た「Unix root 不可」の詳細版です。bypass 下でも効き続ける統制は3つです。

  • deny ルール・明示的な ask ルール・フック(モード判定より前に評価される)
  • コネクタ(ask)・ユーザー対話必須ツール・重要パス削除のコールバックへのフォールスルー
  • クロスセッションメッセージングのセーフガード(第73回の「受信メッセージは承認にならない」系)

plan。 読み取り専用ツールは default と同じように動きます。ファイル編集は allow ルールがマッチしても canUseTool へ回り、v2.1.212 以降はファイルを変えるシェルコマンドも同様です。allowDangerouslySkipPermissions: true と plan を併用しても、計画中の書き込みはコールバックに届きます。このオプションの意味は、後で setPermissionMode() で bypass に切り替えられるようにしておくことです。計画中に Claude が AskUserQuestion で要件を確認してくることがあり、その処理はユーザー入力のページに回されています。


4. まとめ

  • 評価順は フック → deny → ask → モード → allow → canUseTool。モードが allow より先なので、bypassPermissions を allowed_tools で絞ることはできない。絞るなら disallowed_tools を使う。
  • フックの allow は deny/ask を飛ばせず、重要パスの削除も通せない。フックは止める力が強く、通す力は限定的。
  • ask ルール・AskUserQuestion・requiresUserInteraction 付き MCP・組織 ask のコネクタは、bypass 下でもコールバックへ流れる。dontAsk では拒否になる。
  • 自動承認された呼び出しは canUseTool を通らない。TS では CLAUDE_SDK_CAN_USE_TOOL_SHADOWED 警告が出るが、settings.json 由来の allow は検出対象外。全呼び出しのゲートには PreToolUse フックを使う。
  • ルール記法の落とし穴:allow のグロブは mcp__<server>__ の後ろのみ/Write(path) はマッチしないので Edit(path) で書く/絶対パスは // 始まり(/ 始まりは作業ディレクトリ基準)/Bash(rm *) は /bin/rm を捕まえない。
  • settingSources を明示するなら "project" を含めないと、settings.json の権限ルールが消える。
  • dontAsk は「リスト外を拒否」する構成だが、承認不要な呼び出し(読み取り専用 Bash・Agent など)は通る。
  • サブエージェントは親のモードを継承する。慎重な親の下では個別に緩められるが、bypass にはできない(v2.1.267 以降)。

次回予告

次回は 「承認とユーザー入力の処理」(agent-sdk/user-input) を取り上げる予定です。

今回は何度も「canUseTool コールバックへ回る」と書きました。次回は、そのコールバックの中身を扱います。

  • 承認画面を自分のアプリにどう組み込むか
  • 計画中の Claude が AskUserQuestion で確認質問をしてきたとき、どう答えを返すか
  • 組織の設定で承認が必須になったツールの理由を、どう受け取るか

今回の評価順序で「最後の砦」だったステップを、実装する側の目線で読んでいきます。

※テーマは変更になる場合があります。


よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました