tmux運用の次へ ── Claude Agent SDKでツール実行をコードで制御する
前回書いた 「Claude Code × tmuxで『投げて寝る』開発を試してみた」 で、最後に「次はAgent SDKを試したい」と書いていたので、実際にやってみました。
結論から言うと、tmux + claude -p の運用とは別物でした。やれることの粒度が全然違う。
Agent SDKってなに
ざっくり言うと、Claude Codeの機能をPythonやTypeScriptのライブラリとして使えるようにしたものです。
CLIの claude -p だと「プロンプトを渡して結果を受け取る」という一方通行ですが、Agent SDKだとツール実行のたびにコールバックを挟めます。「このファイルは触らせない」「このコマンドはログを残す」みたいな制御をコードで書ける。
前回のtmux運用で感じていた「放置中に何が起きているかわからない」問題、これで解決できそうだなと思ったのがきっかけです。
セットアップ
Pythonの場合はこれだけ。
uv init && uv add claude-agent-sdk
TypeScriptなら。
npm install @anthropic-ai/claude-agent-sdk
APIキーの設定も必要です。
export ANTHROPIC_API_KEY=your-api-key
Bedrock経由で使う場合は CLAUDE_CODE_USE_BEDROCK=1 と AWS認証情報をセットすればOK。自分はBedrock経由で動かしています。
最小限のコード
まず動かしてみます。
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
async for message in query(
prompt="srcディレクトリの構成を教えて",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"],
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(main())
allowed_tools でツールを絞るのはCLIの --allowedTools と同じ考え方ですね。ただしコードなので、条件分岐で動的に変えたりできる。ここがCLIとの大きな違いです。
フック機能が本体
正直、Agent SDKの一番の売りはフック(Hook)だと思います。ツール実行の前後にカスタムロジックを挟めるやつ。
PreToolUse: 実行前に介入する
たとえば .env ファイルへの書き込みをブロックする。
async def protect_env_files(input_data, tool_use_id, context):
file_path = input_data["tool_input"].get("file_path", "")
if file_path.endswith(".env"):
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": ".envファイルは変更できません",
}
}
return {}
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])
]
}
)
tmux運用のときは --allowedTools で「何を許可するか」しか制御できなかったんですが、フックだと「許可した上で、特定条件だけブロック」ができる。この粒度の差は大きいです。
PostToolUse: 実行後にログを取る
ファイル変更を全部記録する例。
from datetime import datetime
async def log_file_changes(input_data, tool_use_id, context):
file_path = input_data.get("tool_input", {}).get("file_path", "unknown")
with open("./agent-audit.log", "a") as f:
f.write(f"{datetime.now().isoformat()}: {input_data['tool_name']} -> {file_path}\n")
return {}
options = ClaudeAgentOptions(
hooks={
"PostToolUse": [
HookMatcher(matcher="Edit|Write", hooks=[log_file_changes])
]
}
)
前回の記事で「放置で動かした後、git diff を全部読んでからcommitしている」と書きましたが、このフックがあると変更の経緯がログに残るので、なぜその変更をしたかの文脈が追いやすくなります。地味にありがたい。
Permission Modeの使い分け
権限管理もCLIより細かく制御できます。
| モード | 動作 |
|---|---|
default |
許可されていないツールはユーザーに確認 |
acceptEdits |
ファイル操作を自動承認 |
bypassPermissions |
全部自動承認(サンドボックス専用) |
dontAsk |
許可されていないツールは自動拒否 |
plan |
ツール実行なし、計画だけ |
自分がよく使うのは dontAsk です。「許可したツール以外は黙って拒否」という動作で、ヘッドレスで回すときに安心感がある。
tmux運用だと --dangerously-skip-permissions か --allowedTools の二択で、中間がなかったんですよね。dontAsk はその中間を埋めてくれる感じです。
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep", "Edit"],
permission_mode="dontAsk",
)
これで「読み書きと検索はOK、Bashは黙って拒否」という設定になります。
実際に組んでみたもの
試しに、PR のコード変更を自動レビューするスクリプトを書いてみました。
import asyncio
import subprocess
from claude_agent_sdk import query, ClaudeAgentOptions
async def review_pr():
diff = subprocess.run(
["git", "diff", "main...HEAD"],
capture_output=True, text=True
).stdout
prompt = f"""以下のdiffをレビューしてください。
セキュリティ上の問題、パフォーマンスの懸念、バグの可能性を指摘してください。
```diff
{diff}
```"""
async for message in query(
prompt=prompt,
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"],
permission_mode="dontAsk",
),
):
if hasattr(message, "result"):
print(message.result)
asyncio.run(review_pr())
読み取り専用のツールだけ渡して、dontAsk で他は拒否。レビューなので書き込みは不要ですし、安全に回せます。
ここで少しハマったのが、diff が大きすぎるとトークン制限に引っかかること。ファイル単位で分割して投げるようにしたら安定しました。
tmux運用との比較
実際に両方使ってみた所感です。
tmux + claude -p が向いているケース
- 一発で完結するタスク(「テストを修正して」「READMEを更新して」)
- 設定が少なくてすぐ実行したい
- 結果を
git diffで確認すれば十分
Agent SDKが向いているケース
- 定期実行したい処理(CI/CD、定時レビュー)
- ツール実行を細かく制御したい
- 複数のタスクをオーケストレーションしたい
- 監査ログが必要
ざっくり言うと、tmuxは「手軽に投げて寝る」、Agent SDKは「ちゃんと制御して回す」という棲み分けです。自分は普段の開発はtmux運用のまま、CI連携やバッチ的な処理にはAgent SDKを使う方向に落ち着きました。
ハマったところ
いくつかメモ。
非同期処理の罠
Agent SDKのAPIは全部 async なので、既存の同期的なスクリプトに組み込むときに少し手間取りました。asyncio.run() で囲めば動くんですが、他のasyncライブラリと組み合わせるときにイベントループの衝突が起きる。自分の環境ではFastAPIのエンドポイント内で呼ぼうとして一度ハマりました。
フックの戻り値
PreToolUse フックで何も返さないとき、空の辞書 {} を返す必要がある。None を返すとエラーになる。ドキュメントにも書いてあるんですが、最初見落としていて少し焦りました。
セッション管理
ClaudeSDKClient を使うと会話のコンテキストを維持できるんですが、セッションのライフサイクル管理は自分でやる必要がある。長時間動かすバッチだとメモリの使用量も気にしたほうがいいかもしれません。
所感
Agent SDK、思っていたよりしっかり作られていました。特にフック機能は、tmux運用で感じていた「放置中の不安」をかなり軽減してくれます。
ただ、全部をAgent SDKに移行するかというと、そうでもない。ちょっとしたタスクを投げるだけなら、tmuxで claude -p を叩くほうが圧倒的に楽です。コードを書かなくていい。
使い分けとしては、「1回きりの作業はtmux、繰り返す処理はAgent SDK」がちょうどいい気がしています。前回の記事の延長線上にあるツールではあるんですが、解決する課題が微妙に違う。どっちかだけで完結するものじゃなくて、両方持っておくと便利、という結論です。
次はAgent SDKでサブエージェントを複数立てて並列にタスクを処理させるやつを試してみたいですね。マルチエージェントの協調動作、うまくハマればCI上でかなり面白いことができそうです。
ここまで読んでいただき、ありがとうございます。もしこの記事の技術や考え方に少しでも興味を持っていただけたら、ネクストのエンジニアと気軽に話してみませんか。
- 選考ではありません
- 履歴書不要
- 技術の話が中心
- 所要時間30分程度
- オンラインOK