0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

can_use_toolはallowed_toolsの丸ごと許可で呼ばれなくなっていた

0
Posted at

はじめに

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_toolallowed_toolspermission_mode に「覆い隠される(shadowed)」条件
  • 実際に3パターン(丸ごと許可/narrow指定/bypassPermissions)を動かした検証結果と警告メッセージ全文
  • SDKソースコードの該当ロジック(_get_can_use_tool_shadowed_warning
  • 自前の権限ゲートを壊さないための回避策

前提環境

  • Python 3.11
  • claude-agent-sdk 0.2.116(2026-07-11リリース)
  • 検証は warnings モジュールで直接コールし、Claude API本体への接続は不要(該当ロジックが同期・ローカル完結のため)

TL;DR

  • can_use_tool を設定していても、allowed_toolsツール名を丸ごと書く(例: "Bash")と、そのツールについてはコールバックが一切呼ばれず自動承認される
  • allowed_toolsnarrow指定(例: "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_toolspermission_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" を丸ごと許可)と ケース3bypassPermissions)では警告が発火し、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のフック運用でも起こり得る同種の落とし穴です。

対策

  1. 全ツール呼び出しを漏れなくゲートしたいなら can_use_tool に頼らず PreToolUse フックを使う(公式推奨)。フックは allowed_tools の許可判定より後段で必ず実行される。
  2. can_use_tool を使い続けたいなら allowed_toolsnarrow指定"Bash(ls:*)" のようにサブコマンド単位)にし、丸ごと許可のエントリを避ける。
  3. permission_mode="bypassPermissions"can_use_tool は原理的に併用不可と理解する(bypassPermissions が全承認を先に確定させるため)。
  4. 意図的にシャドーイングさせている場合(特定ツールは無条件許可し、他ツールだけ 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_toolallowed_toolsbypassPermissions にシャドーイングされる場合の警告が追加された
  • allowed_tools に丸ごとツール名を書くとそのツールでは can_use_tool が一切呼ばれない。narrow指定なら呼ばれる(実機で確認済み)
  • bypassPermissions も同様に can_use_tool を無効化する
  • 全ツール呼び出しを確実にゲートしたいなら PreToolUse フックを使うのが公式推奨

参考リンク

  1. claude-agent-sdk-python PR #1081 — "can_use_tool shadowed by allowed_tools" の警告追加PR

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?