~/.claude/tasks/ を覗いたら、セッションIDのディレクトリが8個並んでいました。中身は 1.json、2.json という連番ファイルと、2バイトの .highwatermark。Claude Code v2.1.251 が TodoWrite を捨てて Task ツールに移った跡です。
~/.claude/tasks に残る連番JSON
タスク1件がファイル1つ
$ ls -la ~/.claude/tasks/da9267a9-d664-4a65-88bc-36f867316413/
total 16
-rw-r--r--@ 1 moha staff 1 Aug 31 20:06 .highwatermark
-rw-r--r--@ 1 moha staff 0 Aug 31 20:06 .lock
-rw-r--r--@ 1 moha staff 236 Aug 31 20:06 1.json
ディレクトリ名はセッションID。タスクを2件作ったセッションなら 1.json と 2.json が並びます。中身はこうです。
{
"id": "1",
"subject": "A: 調査を実施する",
"description": "プロジェクトの現状を調査し、必要な情報を収集する",
"activeForm": "調査中",
"status": "pending",
"blocks": [],
"blockedBy": []
}
IDは "1"。UUIDではなく、セッション内で1から振る連番の文字列です。依存関係を何も登録していないタスクにも blocks と blockedBy の空配列が入ります。
.highwatermark が採番を握っている
.highwatermark はそのセッションで発行した最大IDを保持するだけのファイル。2件作ったセッションなら中身は 2 の1バイト。古いセッションを見ると 59 まで伸びているものもありました。
タスクの状態はセッション単位で閉じています。別セッションから同じリストを読むことはできません。セッションをまたいで情報を渡す話はClaude Codeセッション間通信の仕組み—SendMessageが届かない原因で別途整理しています。
TodoWriteは配列を毎回書き直す
公式ドキュメントの「Task ツールへの移行」セクションに、置き換えの中身が1行でまとまっています。
Task ツールは、単一の
TodoWrite呼び出しを、各新規アイテムのTaskCreateと各ステータス変更のTaskUpdateに分割し、TaskListとTaskGetはモデルが現在のリストを読み戻すために利用可能です。
同じ3タスクで2,676バイト対732バイト
差がどれだけ出るか測りました。「READMEにセクションを3つ足す作業をタスクに登録して1つずつ進めて」という同一プロンプトを、Haiku 4.5 で2回投げます。片方は CLAUDE_CODE_ENABLE_TASKS=0 を付けて TodoWrite に戻した状態。
$ claude -p "READMEにセクションを3つ足す作業を、タスクリストに登録してから1つずつ進めて。" \
--model claude-haiku-4-5-20251001 \
--output-format stream-json --verbose
stream-json から tool_use ブロックの入力だけを抜き出してバイト数を足すと、こうなりました。
Taskツール 呼び出し数/入力バイト: (11, 732)
TodoWrite 呼び出し数/入力バイト: (7, 2676)
呼び出し回数は Task ツールのほうが多い。それでも入力の合計は 27% に収まります。TodoWrite は状態を1つ変えるたびに3件分の配列を丸ごと再送するため、タスク数 N に対して送信量が N の2乗で効いてくる構造です。
2つのモードの差分
| 項目 | TodoWrite | Task ツール |
|---|---|---|
| 更新単位 | todos 配列を毎回まるごと書き直す | TaskCreate で1件追加、TaskUpdate で1件パッチ |
| アイテムの形 | { content, status, activeForm } | { subject, description, activeForm?, metadata? } |
| 依存関係 | 持てない | addBlockedBy / addBlocks |
| 削除 | 配列から外す | status: "deleted" |
| 3タスク処理時の入力合計 | 2,676バイト / 7回 | 732バイト / 11回 |
ストリームを監視するコードの落とし穴
公式ドキュメントは、モデルが出す生のキー名がぶれると明記しています。Claude Code は実行前に id や task_id を taskId へ、active_form を activeForm へ寄せますが、その修復はストリームに反映されません。監視側は正規名だけを見ないほうが安全です。
task_id = (
block.input.get("taskId")
or block.input.get("id")
or block.input.get("task_id")
)
もう1つ、ドキュメントと実物がずれる箇所がありました。SDK側の記述では TaskCreate の tool_result が { task: { id, subject } } で返ることになっています。CLIの --output-format stream-json で観測した tool_result は、こういう文字列でした。
"Task #1 created successfully: README にセクション1を追加"
IDを取りたいなら #1 をパースするか、TaskList の結果スナップショットを読むことになります。
Taskツールを呼ぶ前にToolSearchが1往復入る
実測ログの先頭は TaskCreate ではありませんでした。
TOOL_USE ToolSearch {"query": "select:TaskCreate,TaskList", "max_results": 5}
TOOL_RESULT [{"type":"tool_reference","tool_name":"TaskCreate"},
{"type":"tool_reference","tool_name":"TaskList"}]
TOOL_USE TaskCreate {"subject": "ビルドを実行する", ...}
Task 系は遅延ロード対象で、スキーマを取りにいく ToolSearch が必ず1回先に走ります。MCPサーバーを空にして --strict-mcp-config --mcp-config '{"mcpServers":{}}' で起動しても同じ。初期化メッセージのツール一覧には31個中に TaskCreate が載っているのに、それでも ToolSearch を挟みます。タスク3件の登録に13ターン、25.6秒、$0.074かかったうちの1往復ぶんはこれです。
「Task」で始まるツールは3系統ある
初期化ツール一覧に並ぶ名前を分類すると、まったく別の機能が同じ接頭辞を共有しています。
| ツール名 | 役割 |
|---|---|
Task | サブエージェントを起動する |
TaskOutput / TaskStop | バックグラウンド実行の出力取得と停止 |
TaskCreate / TaskUpdate / TaskList / TaskGet | タスクリストの読み書き |
権限設定で Task を許可しても、タスクリスト側は別物として扱われます。
addBlockedByは逆向きの辺も自動で書く
依存関係は TodoWrite 側に存在しない機能です。
A(DBマイグレーション)→ B(APIデプロイ)→ C(動作確認)という順序制約を登録させます。モデルが出した TaskUpdate は2回だけ。
TOOL_USE TaskUpdate {"taskId": "2", "addBlockedBy": ["1"]}
TOOL_RESULT "Updated task #2 blockedBy"
TOOL_USE TaskUpdate {"taskId": "3", "addBlockedBy": ["2"]}
TOOL_RESULT "Updated task #3 blockedBy"
TOOL_USE TaskList {}
TOOL_RESULT "#1 [pending] DBマイグレーション実施
#2 [pending] APIデプロイ [blocked by #1]
#3 [pending] 動作確認 [blocked by #2]"
触っていないタスクのJSONが書き換わる
モデルは taskId: "2" と "3" にしかパッチを当てていません。それなのにディスク上の 1.json はこうなっていました。
{
"id": "1",
"subject": "DBマイグレーション実施",
"status": "pending",
"blocks": ["2"],
"blockedBy": []
}
blocks: ["2"] は誰も指定していない値です。Claude Code が addBlockedBy を受けた時点で、ブロック元のファイルに逆向きの辺を書き足しています。依存は片側から宣言すれば足り、addBlocks で対称の記述を入れる必要はありません。両方書くと同じ辺が二重に登録されます。
ブロッカーが完了すると表示が消える
解除の手続きも要りません。A をビルド、B をデプロイとして依存を張り、A だけ completed にした直後の TaskList がこれです。
(完了前)
#1 [pending] ビルド
#2 [pending] デプロイ [blocked by #1]
(#1 を completed にした後)
#1 [completed] ビルド
#2 [pending] デプロイ
[blocked by #1] が落ちました。TaskUpdate で blockedBy から手動で外す操作は不要です。
依存を張ると何が変わるか
マイグレーションを流す前にデプロイを始める、という順序違反を止められます。依存を張っていないタスクリストは、完了済みかどうかしか持っていないので順番を守らせる手がかりになりません。blockedBy が入っていれば、モデルが TaskList を読み戻すたびに [blocked by #1] が目に入ります。CLAUDE.md に書いた順序ルールは会話が伸びるほど埋もれますが、リストの表示は毎回この位置に出てきます。
status: deletedでファイルごと消える
不要になったタスクは TaskUpdate で status: "deleted" にします。pending / in_progress / completed と同じ列に並ぶ値ですが、挙動だけ違う。
TOOL_USE TaskUpdate {"taskId": "2", "status": "deleted"}
TOOL_RESULT "Updated task #2 deleted"
TOOL_USE TaskList {}
TOOL_RESULT "#1 [pending] A: 調査を実施する"
ディスクからも 2.json が消えます。残るのは 1.json だけ。ただし .highwatermark の中身は 2 のままでした。IDは再利用されないので、次に作るタスクは #3 になります。ログを後から突き合わせるときに番号が飛んでいても、それは欠番ではなく削除済みの跡です。
TodoWriteに戻すCLAUDE_CODE_ENABLE_TASKS=0
TypeScript Agent SDK 0.3.142 および Claude Code v2.1.142 以降、セッションは TodoWrite ではなく Task ツールを使います。Python SDK はパッケージ版数ではなく、起動する CLI のバージョンで決まる点が独特で、cli_path で指定したコピーが v2.1.142 以降かどうかが判定基準になります。
既存の監視コードを書き換えるまでの逃げ道が CLAUDE_CODE_ENABLE_TASKS=0 です。付けて実行すると、モデルは ToolSearch で TodoWrite を引いてきます。
$ CLAUDE_CODE_ENABLE_TASKS=0 claude -p "..." --output-format stream-json --verbose
TOOL_USE ToolSearch {"query": "select:TodoWrite", "max_results": 1}
TOOL_USE TodoWrite {"todos": [{"content": "Add Installation section to README",
"status": "pending",
"activeForm": "Adding Installation section to README"}, ...]}
戻せるのは形だけで、依存関係は失われます。同じ3タスクで入力が2,676バイトに膨らむのも先ほど見たとおり。block.name === "TodoWrite" でマッチしている既存のダッシュボードを、書き換えるまでのあいだ動かし続ける値です。
まとめ
- Claude Code v2.1.142 以降、タスクリストは
TodoWriteからTaskCreate/TaskUpdate/TaskList/TaskGetの4本に分割された - 同じ3タスクの処理で、モデルが送る入力は 2,676バイト → 732バイト(27%)に減った
- 実体は
~/.claude/tasks/<セッションID>/<連番>.json。セッションをまたいだ共有はできない
addBlockedByを片側に張ると、ブロック元の JSON にblocksが自動で書き足される- ブロッカーが
completedになれば[blocked by #N]は自動で外れる status: "deleted"はファイルごと削除するが、.highwatermarkは据え置きでIDは再利用されない
tool search が有効な環境では ToolSearch の1往復が必ず先に入ります。タスク登録を含む短い -p 実行で13ターン・25.6秒・$0.074。3ステップ未満の作業でタスクリストを作らせるのは、この往復のぶんだけ損をします。