Agent SDKでsession_state_changedを受け取る設定と3つの状態
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTSを1にすると、Agent SDKのストリームにrunning・idle・requires_actionの状態が流れます。読むときの注意点も挙げます。
session_state_changedは環境変数を立てたときだけ流れる
session_state_changed は、Claude Codeがセッションの状態を報告するときにメッセージストリームへ出すシステムメッセージです。既定では流れてきません。環境変数 CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS を 1 にしたときだけ、ストリームに加わります。
この変数が効く実行形態は2つです。
| 実行形態 | 必要な条件 |
|---|---|
| Agent SDK | 必要な条件環境変数を 1 にするだけ |
| CLIの非対話実行 | 必要な条件--print、--output-format stream-json、--verbose の3つをそろえる |
CLIで --verbose を付け忘れると、メッセージは出ません。
メッセージの型はTypeScriptのリファレンスにそのまま載っています。
type SDKSessionStateChangedMessage = {
type: "system";
subtype: "session_state_changed";
state: "idle" | "running" | "requires_action";
uuid: UUID;
session_id: string;
};SDKMessage のユニオン型にも SDKSessionStateChangedMessage が含まれます。受け取る側は type === "system" かつ subtype === "session_state_changed" で絞り込めば十分です。
3つの状態が何を意味するか
state に入る値は3つだけです。
state の3つの値
running
セッションが作業中です。
idle
Claude Codeが次のプロンプトを待っています。
requires_action
ホスト側に送ったリクエストへの回答待ちで止まっています。権限確認のプロンプトが典型例です。
requires_action は「Claudeが考えている」でも「入力待ち」でもありません。自分のアプリが何かに答えないと先へ進めない状態です。画面にスピナーを出し続けると、ユーザーが承認ボタンを押せないまま固まって見えます。この状態だけは、UI上で目立たせる価値があります。
Agent SDKで受け取る最小コード
SDKではサブプロセスの環境を env オプションで渡します。env を指定すると process.env とのマージではなく置き換えになるので、PATH などを失わないよう展開して渡します。
import { query } from "@anthropic-ai/claude-agent-sdk";
const stream = query({
prompt: "テストを実行して失敗を要約して",
options: {
env: {
...process.env,
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
},
},
});
for await (const message of stream) {
if (message.type === "system" && message.subtype === "session_state_changed") {
console.log(`state=${message.state} session=${message.session_id}`);
}
}シェルから export CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1 してからSDKを起動しても、環境が継承されるなら同じ結果になるはずです。ただし本番では、コード内で env に明示するほうが、どの環境で動いても挙動が変わりません。
上のコードは公式リファレンスの型定義をもとに組んだ例です。実行結果の出力例は示していません。自分の環境で一度流して、状態の並びを確かめてください。
Pythonで受け取る場合の違い
PythonのSDKには session_state_changed 専用のクラスがありません。専用のデータクラスを持たないサブタイプは、SystemMessage(subtype と data を持つ)として届きます。状態の値は message.data["state"] から読みます。
env の扱いもTypeScriptと逆です。TypeScriptの env は子プロセスの環境を置き換えるのに対し、Pythonの env は引き継いだ環境の上にマージされます。PATH を展開して渡す必要はなく、立てたい変数だけを書けば足ります。
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, SystemMessage
options = ClaudeAgentOptions(
env={"CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS": "1"},
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("テストを実行して失敗を要約して")
async for message in client.receive_messages():
if isinstance(message, SystemMessage) and message.subtype == "session_state_changed":
print(message.data["state"])
asyncio.run(main())落とし穴は、ループの選び方です。receive_response() は ResultMessage で止まるので、result より後に届いた session_state_changed を取りこぼします。状態を追うなら receive_messages() を使い、ループを抜ける条件は自分で決めます。
CLIの非対話実行で確かめる
SDKを組む前に、CLIで状態の並びを眺める方法もあります。次のコマンドは3点セットをそろえ、状態メッセージだけを jq で抜き出す例です。
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1 \
claude -p "package.json の scripts を一覧にして" \
--output-format stream-json --verbose \
| jq -c 'select(.subtype == "session_state_changed") | .state'jq は別途インストールが必要です。出力は1行1状態になります。
権限確認が要る作業を頼んでも、requires_action が出るとは限りません。-p で canUseTool も --permission-prompt-tool も付けていない実行は、確認が要る操作を拒否して先へ進みます。ホスト側に答える相手がいないので、ストリームに現れるのは permission_denied メッセージ側です。requires_action を観察したいときは、canUseTool を持つSDKアプリか、--permission-prompt-tool で渡したMCPツールを用意します。idle と result の順序を自分の環境で確かめたいときも、同じコマンドが使えます。select の条件から .subtype を外して .type を併せて出せば、状態メッセージと result が届いた順に並びます。1回の実行で順序が決まって見えても、リファレンスは順序を保証していないので、そこからコードの前提を作らないでください。権限確認の経路はAgent SDKのcanUseToolの記事にまとめています。
requires_actionが立つ条件と、立たない条件
requires_action は「ホストに送った質問へ答えが返るまで止まる」状態です。質問を受ける側がいなければ、立つ余地がありません。権限確認の扱いごとに見ると、次の表のようになります。
| 権限確認の扱い | 確認が要る操作の結果 |
|---|---|
canUseTool コールバックあり(permissionPrompts は既定の host) | 確認が要る操作の結果コールバックへ送られ、答えが返るまで待つ |
--permission-prompt-tool でMCPツールを指定 | 確認が要る操作の結果そのツールへ送られ、答えを待つ |
どちらも無い -p / query() | 確認が要る操作の結果拒否され、実行は続く |
--permission-prompts none(v2.1.259以降) | 確認が要る操作の結果コールバックやMCPツールがあっても拒否される |
状態の並びは、コールバックがある実行ならおおよそ次の順になります。
canUseToolで確認を待つときの状態
- 1
running
Claudeが作業し、確認の要る操作に当たります。
- 2
requires_action
canUseToolに質問が渡り、答えが返るまで実行が止まります。自前UIでは、ここで承認ダイアログを出します。 - 3
running
コールバックが許可か拒否を返すと、作業が再開します(再開の合図としてこの状態が来ることは、型の説明から読み取れる範囲の推測です)。
- 4
idle / result
ターンが終わると
idleとresultが届きます。届く順序は決まっていません。
無人運用で requires_action を待ち続けないための手段が --permission-prompts none です。確認が要る操作はコールバックを呼ばずに拒否され、Claudeには「承認できる人がいないので再試行しないこと」が伝わります。
現在の状態として読む(遷移として読まない)
メッセージ名は state_changed ですが、遷移の通知として扱うと誤ります。リファレンスは、同じ状態が複数回報告されることがあるので、1通を「現在の状態」として読むよう書いています。
これは実装に直結します。
idleが2通続いても、2回目のターンが終わったとは限りません- 前回と同じ値なら何もしない、という判定をコード側に入れておく
- 「runningからidleへ変わった」ことをトリガーにする処理は、重複して動く
状態の通知をそのままジョブの完了トリガーにすると、二重実行の原因になります。
let last: string | undefined;
for await (const message of stream) {
if (message.type === "system" && message.subtype === "session_state_changed") {
if (message.state === last) continue; // 同じ状態の再通知は無視
last = message.state;
updateUi(message.state); // 自前のUI更新関数(例)
}
}idleとresultは順不同で届く
もう1つの注意点が順序です。1ターンの idle メッセージと result メッセージは、どちらが先に届くか決まっていません。
result を待ってから idle を見る、という前提でコードを書くと、ターン終了直後の取りこぼしや二重処理につながります。result だけを待つ実装なら順序に左右されず、idle だけを待つ実装は、result の中身を読む前にターンを閉じてしまう恐れがあります。次のどちらかに寄せると安全です。
- ターンの完了判定は
resultメッセージだけで行い、idleは表示の更新に使う - 両方がそろった時点で完了とみなす
バックグラウンド作業がある間のidle
バックグラウンドのサブエージェントやワークフローが動いている間、idle を出すかどうかは別の環境変数 CLAUDE_CODE_BG_TASKS_REPORT_RUNNING で変わります。
- 非対話セッションの既定は、ターンが終わってもバックグラウンド作業が生きている間はrunningを報告し続ける動き
0にすると、作業が残っていてもターン終了のたびにidleを報告する- 既定の動きと
0による切り替えにはClaude Code v2.1.269以降が必要。それ以前の版では1を設定するとrunningを保持する - 開発サーバーのようなバックグラウンドのシェルコマンドは、runningの保持に数えない
この変数は非対話セッションの話で、対話セッションの画面表示とは関係しません。なお、バックグラウンドのシェルコマンドが動いているだけの間は、通常どおりターン終了で idle が届きます。
リモートのセッション一覧のように状態を監視する画面では、既定のままのほうが「Claudeは入力待ち」という誤表示が出にくくなります。逆に、ターンの区切りごとに次のプロンプトを送りたい自動化では、0 にしてidleを拾う設計もありえます。どちらが合うかは、バックグラウンド作業を使うかで決まります。
idleのあとに届く通知ターンの見分け方
idle は「もう何も起きない」という意味ではありません。バックグラウンドの作業が終わると、完了通知がセッションに届いて新しいターンが始まることがあります。このターンは人が打ったプロンプトではないので、running に戻っても「ユーザーの操作の結果」と決めつけられません。
見分けるには、SDKUserMessage か SDKResultMessage の origin.kind が "task-notification" かを確かめます。通知の文面での判定は、リファレンスが勧めていません。通知の発生元は同じ origin の subkind で分かります。状態メッセージだけで「いまのターンは誰が始めたか」を決めるコードは、バックグラウンド作業を使った途端に崩れます。
挙動を左右するバージョンの条件
状態まわりの挙動は、Claude Codeのバージョンで変わる箇所がいくつかあります。手元の動きが記事と合わないときは、先に版を疑ってください。
| 挙動 | 必要なバージョン |
|---|---|
permission_denied メッセージが、コールバックなしの実行でも出る | 必要なバージョンv2.1.223以降(それ以前はこの実行で出ない) |
--permission-prompts none で確認を一律に拒否する | 必要なバージョンv2.1.259以降 |
バックグラウンド作業中も running を報告する既定と、0 による切り替え | 必要なバージョンv2.1.269以降 |
| 通知ターンにも「人の入力ではない」旨の注記が付く | 必要なバージョンv2.1.205以降(それ以前は、idle中に届いた通知に付かない) |
バージョンの確認は claude --version で足ります。
たとえば permission_denied が出ないと感じたときは、v2.1.223より前の版で、canUseTool も permissionPromptToolName も指定していない可能性があります。逆に、バックグラウンドのサブエージェントを動かしているのに idle が早く届くなら、v2.1.269より前の版か、CLAUDE_CODE_BG_TASKS_REPORT_RUNNING=0 が効いているかのどちらかです。
使い方別に、どのメッセージで何を判定するか
同じ session_state_changed でも、見る場所は用途で変わります。
| 用途 | 状態メッセージの使い道 | 完了や結果の判定 |
|---|---|---|
| 自前UIのステータス表示 | 状態メッセージの使い道running でスピナー、idle で入力欄を有効化、requires_action で確認ダイアログへ誘導 | 完了や結果の判定result |
| セッション一覧の監視 | 状態メッセージの使い道どのセッションが人の応答待ちかを一覧に出す | 完了や結果の判定不要(状態だけ見る) |
| 自動化の待機制御 | 状態メッセージの使い道次のプロンプトを送る目安にする | 完了や結果の判定result(idle だけに頼らない) |
結果の取得だけが目的なら、状態メッセージは不要です。result を読めば足ります。出力を逐次描画したい場合は、状態ではなくトークン単位のイベントを扱うAgent SDKのストリーミング出力の領分です。ターンがどう回るかの前提はAgent SDKのエージェントループにまとめています。
まとめ
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1 は、running・idle・requires_actionの3値をストリームに足す設定です。SDKなら環境変数だけ、CLIなら --print と stream-json と --verbose が要ります。読むときは、1通を現在の状態として扱い、重複を捨て、完了判定は result に任せる。この3点を守れば、状態メッセージは表示と監視にそのまま使えます。Pythonでは SystemMessage の data から読み、receive_messages() で回します。