はじめに
Claude Agent SDK(Python版)には、ツール呼び出しごとに独自の許可判定を挟める can_use_tool というコールバックがあります。「このツールは自分のロジックで許可・拒否を決めたい」というときに使う仕組みで、SDKを使ってカスタムの権限ゲートを組むエージェント開発者にとっては中核機能の1つです。
ところが2026年7月6日リリースの claude-agent-sdk v0.2.111で、この can_use_tool が 設定していても一切呼ばれないケースがある ことに対する警告(CanUseToolShadowedWarning)が追加されました1。原因は allowed_tools の書き方1つで、コールバックが「シャドーイング(覆い隠し)」されてしまうという挙動です。
本記事では実際にSDKをインストールし、3パターンのオプション設定でこの警告が出るか・出ないかをコードで再現しました。「動くはずのコールバックが実は呼ばれていなかった」は気づきにくい静かな不具合(サイレントフェイル)であり、Claude Code のフック(PreToolUse等)や本プロジェクト自身が使っている許可リスト運用にも通じる示唆があるため、仕組みごと共有します。
この記事で学べること
-
can_use_toolがallowed_toolsやpermission_modeに「覆い隠される(shadowed)」条件 - 実際に3パターン(丸ごと許可/narrow指定/bypassPermissions)を動かした検証結果と警告メッセージ全文
- SDKソースコードの該当ロジック(
_get_can_use_tool_shadowed_warning) - 自前の権限ゲートを壊さないための回避策
前提環境
- Python 3.11
-
claude-agent-sdk0.2.116(2026-07-11リリース) - 検証は
warningsモジュールで直接コールし、Claude API本体への接続は不要(該当ロジックが同期・ローカル完結のため)
TL;DR
-
can_use_toolを設定していても、allowed_toolsに ツール名を丸ごと書く(例:"Bash")と、そのツールについてはコールバックが一切呼ばれず自動承認される -
allowed_toolsを narrow指定(例:"Bash(ls:*)"のようにコマンド単位に絞る)にすれば、範囲外の呼び出しはちゃんとcan_use_toolに届く -
permission_mode="bypassPermissions"も同様にcan_use_toolを完全に無効化する - 全ツール呼び出しを漏れなくゲートしたいなら
can_use_tool単体ではなく PreToolUse フック を使う(SDK公式の推奨)
背景: can_use_tool と allowed_tools の役割分担
ClaudeAgentOptions には権限制御に関わるオプションが複数あります。
-
allowed_tools: 特定のツール(または特定の呼び出しパターン)を無条件に許可するリスト -
permission_mode:"default"/"acceptEdits"/"plan"/"bypassPermissions"/"dontAsk"/"auto"のいずれかでエージェント全体の権限モードを決める -
can_use_tool: ツール呼び出しごとに呼ばれ、{"behavior": "allow" | "deny", ...}を返す任意のコールバック関数
この3つは独立した設定に見えますが、実際には allowed_tools と permission_mode の判定が can_use_tool より先に評価される という優先順位があります。つまり allowed_tools で先に「許可」が確定してしまえば、can_use_tool の出番はそもそも来ません。
自前のロジックで「このBashコマンドは危険だから拒否する」といった判定を can_use_tool に書いたつもりが、allowed_tools=["Bash"] を同時に設定していたせいで そのロジックが一度も実行されていなかった ——という事故が起こり得るわけです。
実際に検証してみた
_warn_if_can_use_tool_shadowed はSDK内部でオプション構築のたびに1回呼ばれる同期関数で、Claude API本体への接続なしにローカルで直接テストできます。仮想環境を作ってSDKをインストールし、3パターンを試しました。
python3 -m venv sdk-test-venv
source sdk-test-venv/bin/activate
pip install claude-agent-sdk==0.2.116
検証コード:
import warnings
from claude_agent_sdk.types import (
ClaudeAgentOptions,
PermissionResultAllow,
_warn_if_can_use_tool_shadowed,
)
async def fake_can_use_tool(tool_name, input_data, context):
return PermissionResultAllow(updated_input=input_data)
def check(label, **kwargs):
opts = ClaudeAgentOptions(can_use_tool=fake_can_use_tool, **kwargs)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
_warn_if_can_use_tool_shadowed(opts)
print(f"=== {label} ===")
if not w:
print("(警告なし)")
for warning in w:
print(f"[{warning.category.__name__}] {warning.message}")
print()
check("ケース1: allowed_tools=['Bash'](丸ごと許可)", allowed_tools=["Bash"])
check("ケース2: allowed_tools=['Bash(ls:*)'](narrow指定)", allowed_tools=["Bash(ls:*)"])
check("ケース3: permission_mode='bypassPermissions'", permission_mode="bypassPermissions")
実行結果
=== ケース1: allowed_tools=['Bash'](丸ごと許可) ===
[CanUseToolShadowedWarning] can_use_tool will not be invoked for: Bash. An allowed_tools
entry that allows a whole tool auto-approves it before the callback is consulted.
To gate every tool call, use a PreToolUse hook; or narrow the entry so calls fall
through to can_use_tool. Allow rules from settings files can also shadow the
callback but are not visible here.
=== ケース2: allowed_tools=['Bash(ls:*)'](narrow指定) ===
(警告なし)
=== ケース3: permission_mode='bypassPermissions' ===
[CanUseToolShadowedWarning] can_use_tool will not be invoked: permission_mode
'bypassPermissions' auto-approves every tool call (except explicit deny rules)
before the callback is consulted. To gate every tool call, use a PreToolUse hook
instead.
ケース1("Bash" を丸ごと許可)と ケース3(bypassPermissions)では警告が発火し、Bashツール呼び出しに対して can_use_tool が二度と呼ばれないことが明示されました。一方 ケース2("Bash(ls:*)" のように ls コマンドだけに絞ったnarrow指定)では警告が出ず、ls 以外のBashコマンドはちゃんと can_use_tool に届く設計になっていることを確認できました。
なぜこうなるのか(該当ロジック)
SDKのソース(claude_agent_sdk/types.py、MITライセンス)を見ると、判定はシンプルな文字列パースです。
def _whole_tool_allowed(entry: str) -> str | None:
"""entryが特定のコマンドに絞られていない『丸ごと許可』ならツール名を返す"""
if not entry.strip():
return None
open_index = entry.find("(")
if open_index == -1:
return entry
if open_index == 0 or not entry.endswith(")"):
return None
return entry[:open_index] if entry[open_index + 1 : -1] in ("", "*") else None
"Bash" や "Bash()" "Bash(*)" は「丸ごと許可」と判定されて can_use_tool をシャドーイングし、"Bash(ls:*)" のように具体的な指定が入っている場合だけ判定対象から外れます。CLIのルールパーサーと同じ挙動をPython側でも再現している、とコメントにも明記されています。
この警告はPython SDK固有ではなく、TypeScript SDK側でも同じ条件を CLAUDE_SDK_CAN_USE_TOOL_SHADOWED というプロセス警告コードで報告しているとソースコードのコメントに書かれており、両SDKで仕様が揃っています。
実務への影響
この挙動が厄介なのは、エラーにならず、コードは「動いているように見える」 点です。can_use_tool に危険なコマンドを拒否するロジックを書いても、allowed_tools=["Bash"] を足した瞬間にそのロジックがまるごとバイパスされます。しかも警告はPythonの warnings 経由なので、標準出力をログに流していないパイプラインでは気づかないまま本番稼働してしまう可能性があります。
同じ構図は、本プロジェクトが .claude/hooks/ で使っているような「コマンド名の許可リスト+フックでの追加検証」という二段構えの権限運用にも当てはまります。「許可リストで通した後にさらに独自ロジックで絞る」つもりが、許可リストの書き方次第で独自ロジックそのものがスキップされる のは、SDKでもCLIのフック運用でも起こり得る同種の落とし穴です。
対策
-
全ツール呼び出しを漏れなくゲートしたいなら
can_use_toolに頼らず PreToolUse フックを使う(公式推奨)。フックはallowed_toolsの許可判定より後段で必ず実行される。 -
can_use_toolを使い続けたいならallowed_toolsを narrow指定("Bash(ls:*)"のようにサブコマンド単位)にし、丸ごと許可のエントリを避ける。 -
permission_mode="bypassPermissions"とcan_use_toolは原理的に併用不可と理解する(bypassPermissions が全承認を先に確定させるため)。 - 意図的にシャドーイングさせている場合(特定ツールは無条件許可し、他ツールだけ
can_use_toolでゲートしたいケース)は警告が出ても問題ない。warnings.filterwarnings("ignore", category=CanUseToolShadowedWarning)で明示的にミュートできる。
著者視点の発見ポイント
筆者が実際に検証して意外だったのは、「can_use_tool を設定さえすれば全ツール呼び出しが自分のコールバックを通る」という直感が、allowed_tools の書き方次第で簡単に裏切られる点でした。ドキュメントの can_use_tool の説明文だけを読むと「ツール呼び出しごとに呼ばれる」としか書いておらず、allowed_tools との優先順位までは明記されていません。実際に3パターンを動かして初めて「丸ごと許可か、narrow指定か」という書式の違いが権限ゲートの有効・無効を左右すると分かりました。カスタム権限ロジックを書くなら、リリースノートを読むだけでなく allowed_tools を意図的に空にした状態と併用した状態の両方で 一度は警告の有無を自分の目で確認する ことをおすすめします。
まとめ
- Claude Agent SDK(Python版)v0.2.111で
can_use_toolがallowed_toolsやbypassPermissionsにシャドーイングされる場合の警告が追加された -
allowed_toolsに丸ごとツール名を書くとそのツールではcan_use_toolが一切呼ばれない。narrow指定なら呼ばれる(実機で確認済み) -
bypassPermissionsも同様にcan_use_toolを無効化する - 全ツール呼び出しを確実にゲートしたいなら PreToolUse フックを使うのが公式推奨
参考リンク
- claude-agent-sdk-python v0.2.111 リリースノート — 警告追加のリリース(2026-07-06)
- claude-agent-sdk PyPI — パッケージ情報・バージョン履歴
- Claude Agent SDK overview — 公式ドキュメント
-
claude-agent-sdk-python PR #1081 — "can_use_tool shadowed by allowed_tools" の警告追加PR ↩