はじめに / 対象と前提
Claude Code をターミナルで使い慣れてきて、次は「自分の Python スクリプトやバッチ処理から、同じエージェントを呼び出したい」と思った人向け。Claude API(anthropic パッケージ)を直接叩くのではなく、Claude Code のツール実行・許可制御・コンテキスト管理をそのまま借りるのが Claude Agent SDK の位置づけだ。
自分が動作確認した環境は以下。
- macOS 15 / Python 3.13 / Node.js 22.x
- Claude Code v2.x(
claude --versionで確認) -
claude-agent-sdk0.1.x(pip) - 認証は環境変数
ANTHROPIC_API_KEY
SDK はバージョンアップでデフォルト挙動が変わっている(後述の setting_sources が典型)。読んでいる時点のバージョンを必ず確認してほしい。
TL;DR
-
pip install claude-agent-sdkだけでは動かない。SDK は Claude Code CLI を子プロセスで起動する薄いラッパーなので、claudeコマンドが解決できる必要がある - 一発実行は
query()、会話を続けるならClaudeSDKClient。どちらもClaudeAgentOptionsにcwd/allowed_tools/permission_modeを渡す - ハマりどころは 3 つ。
CLINotFoundError、非対話なのに許可待ちで止まる(またはツールが黙って拒否される)、CLAUDE.mdが読み込まれない
手順 / 動かし方
1. インストールと疎通確認
npm install -g @anthropic-ai/claude-code
claude --version # 2.x.x が出ればOK
python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk
python -c "import claude_agent_sdk; print('ok')"
2. 最小コード:読み取り専用でリポジトリを調査させる
import anyio
from claude_agent_sdk import (
query, ClaudeAgentOptions,
AssistantMessage, ResultMessage, TextBlock, ToolUseBlock,
)
async def main():
options = ClaudeAgentOptions(
cwd="/path/to/your-repo",
allowed_tools=["Read", "Grep", "Glob"], # 読み取り系だけ許可
permission_mode="default",
max_turns=10,
)
async for msg in query(
prompt="このリポジトリのテスト実行コマンドと、テストが置かれているディレクトリを調べて報告して",
options=options,
):
if isinstance(msg, AssistantMessage):
for block in msg.content:
if isinstance(block, ToolUseBlock):
print(f"[tool] {block.name} {block.input}")
elif isinstance(block, TextBlock):
print(block.text)
elif isinstance(msg, ResultMessage):
print(f"--- turns={msg.num_turns} cost=${msg.total_cost_usd:.4f}")
anyio.run(main)
実行結果(抜粋):
[tool] Glob {'pattern': '**/pytest.ini'}
[tool] Read {'file_path': '/path/to/your-repo/pyproject.toml'}
テストは `pytest` で実行します。設定は pyproject.toml の [tool.pytest.ini_options] にあり、
テストファイルは tests/ 配下に 14 本あります。
--- turns=4 cost=$0.0312
query() は async generator で、CLI が吐くメッセージを順に流してくる。ResultMessage が最後に 1 回だけ来て、ターン数とコストが取れる。バッチで回すならここをログに残すのが基本。
3. 編集を伴うタスクにする
ファイルを書かせるなら Edit / Write / Bash を許可し、permission_mode を acceptEdits にする。
options = ClaudeAgentOptions(
cwd="/path/to/your-repo",
allowed_tools=["Read", "Grep", "Glob", "Edit", "Write", "Bash"],
permission_mode="acceptEdits", # cwd 配下のファイル編集を自動承認
max_turns=30,
)
4. 会話を続ける:ClaudeSDKClient
「まず調査 → 結果を見てから直す」のように、前のターンの文脈を引き継ぎたいときは ClaudeSDKClient を使う。query() は毎回新規セッションになる。
from claude_agent_sdk import ClaudeSDKClient
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("失敗しているテストを列挙して。まだ直さないで")
async for msg in client.receive_response():
... # 上と同じ出力処理
await client.query("1 件目だけ直して、pytest でそのテストが通ることを確認して")
async for msg in client.receive_response():
...
ハマりどころ
1. CLINotFoundError: Claude Code not found
claude_agent_sdk._errors.CLINotFoundError: Claude Code not found.
Install with: npm install -g @anthropic-ai/claude-code
原因:SDK は claude 実行ファイルを PATH から探して子プロセス起動する。ターミナルでは動くのに、launchd / cron / systemd / Docker から起動すると PATH に npm のグローバル bin(/opt/homebrew/bin や ~/.npm-global/bin)が入っておらず見つからない。
回避策:which claude で絶対パスを調べて cli_path に渡す。
options = ClaudeAgentOptions(
cli_path="/opt/homebrew/bin/claude",
...
)
自分はこれをスケジューラ経由で踏んだ。対話シェルで再現しないので原因に気づくまで時間を溶かした。
2. 非対話なのに許可待ちで止まる / ツールが黙って拒否される
症状 A:permission_mode="default" で allowed_tools に入れていないツール(例:Bash)を Claude が使おうとすると、対話 UI が無いので承認できず、Claude が「権限がないため実行できませんでした」とテキストで返して普通に終了する。ResultMessage.is_error は False。ログを見ないと失敗に気づけない。
症状 B:can_use_tool コールバックで動的に判定しようとして query() に渡すと、コールバックはストリーミング入力モードでしか使えないというエラーになる。
回避策:
- 使わせたいツールは
allowed_toolsに明示する。Bashを許可すると全コマンドが通るので、必要ならdisallowed_toolsと併用する - 動的判定が要るなら
ClaudeSDKClient+can_use_toolにする
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
async def gate(tool_name, input_data, context):
if tool_name == "Bash":
cmd = input_data.get("command", "")
if "rm -rf" in cmd or "git push" in cmd:
return PermissionResultDeny(message=f"blocked: {cmd}")
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash"],
can_use_tool=gate,
...
)
- 「返答テキストに『権限』『permission』が含まれたら失敗扱い」のような後段チェックを自分で入れる。SDK は仕事が完遂されたかどうかまでは判定してくれない
permission_mode="bypassPermissions" は全部通すので手っ取り早いが、コンテナなど壊れてもいい環境以外では使わないほうがいい。
3. CLAUDE.md も .claude/settings.json も読まれない
症状:ターミナルの Claude Code では守られていたプロジェクト規約(CLAUDE.md)を SDK 経由だと無視する。カスタムスラッシュコマンドや Skills も認識しない。
原因:SDK v0.1.0 以降、setting_sources のデフォルトが 「何も読まない」 に変わった。SDK アプリの挙動がユーザーのローカル設定に左右されないようにするための仕様変更だが、旧バージョンの記事を見て書くと必ず踏む。
回避策:読ませたいソースを明示する。
options = ClaudeAgentOptions(
setting_sources=["project"], # CLAUDE.md / .claude/ を読む
system_prompt={
"type": "preset",
"preset": "claude_code", # Claude Code 標準のシステムプロンプト
"append": "回答は日本語で。変更前に必ず既存テストを実行すること。",
},
...
)
system_prompt も同様で、未指定だと Claude Code のデフォルトシステムプロンプトは使われない。ターミナルと同じ振る舞いを期待するなら preset: claude_code を指定する。
背景・補足
なぜ CLI 依存の問題がそのまま出るのかは、構造を見ると腑に落ちる。
ツール実行も許可判定も設定ファイルの読み込みも、全部 CLI 側で起きている。だから PATH・環境変数・設定ソースといった CLI の実行環境の問題が SDK の問題として顔を出す。逆に言えば、CLI で再現テストをすれば切り分けが早い。
まとめ
-
claude-agent-sdkは CLI のラッパー。pipとnpmの両方が必要で、非対話環境ではcli_pathを絶対パスで渡す - 使わせるツールは
allowed_toolsに明示し、編集させるならpermission_mode="acceptEdits"。拒否されてもis_errorは立たないので後段チェックを入れる -
setting_sourcesとsystem_promptはデフォルトで空。ターミナルと同じ挙動が欲しければ["project"]とpreset: claude_codeを明示する - 動的な許可判定は
ClaudeSDKClient+can_use_tool。query()では使えない -
ResultMessageのコストとターン数は必ずログに残す