朝いちばんに、いつもの手順を Claude Code に渡しました。個人開発で複数のアプリとサイトを並行して運用しているため、リリース前の確認やサイト側の定期メンテナンスは、決まった順序の多段作業として任せています。
その日は、画面の様子が違いました。作業自体は進んでいるのに、いつも左側に積み上がっていく進捗のチェックリストが、最後まで空のままだったのです。
不思議だったのは、同じ手順を定期実行に載せている方では、タスクが今までどおり並んでいたことでした。壊れたなら両方で壊れるはずです。片方だけ静かに消える現象には、たいてい設定ではなく前提の変更が隠れています。
v2.1.233 で変わったのは「ツールを読み込むかどうか」だった
原因は自分の設定ではありませんでした。Claude Code v2.1.233 以降、TodoWrite・TaskCreate・TaskGet・TaskUpdate・TaskList の5つは、Opus 4.8・Sonnet 5・Fable 5・Mythos 5 とそれ以降の同系モデルでは、明示的に希望しない限り読み込まれなくなりました。
理由は公式のツールリファレンスに書かれています。これらのモデルは書き出したチェックリストがなくても多段の作業を追えるうえ、ツール定義とそれに付随するリマインダーがコンテキストを占めるため、既定では外すという判断です。読み込まれていない以上、作業中にタスクリストへ何かが積まれることもありません。
つまり「タスクが増えない」のではなく「タスクを増やす道具が最初から手元にない」状態でした。ここを取り違えると、プロンプトの書き方やモデルの理解力を疑う方向に時間を溶かします。
紛らわしいのは、同じ「Todo を使いたい」という目的に対して、モデルによって触る変数が逆向きに効くことです。
セッションのモデル 既定で読み込まれるもの 変えたいときに触る変数
Opus 4.8 / Sonnet 5 / Fable 5 / Mythos 5 以降 5つとも読み込まれない CLAUDE_CODE_ENABLE_TODO_TOOLS=1 で足す
それ以外(Opus 4.7 など) Task 系4つが読み込まれる CLAUDE_CODE_ENABLE_TASKS=0 で TodoWrite に戻す
前者は足し算、後者は引き算です。同じ画面を目指しているのに操作の向きが違うので、以前の環境の設定をそのまま持ち込むと噛み合いません。
環境変数を足す前に確かめる3つの分岐
CLAUDE_CODE_ENABLE_TODO_TOOLS=1 を付ければ全モデル・全プロバイダで5つとも戻ります。ただ、その前に切り分けておかないと、戻したのに直らない、あるいは直ったように見えて別の場所で食い違う、という二次被害が起きます。私が実際に順番に潰したのは次の3点でした。
1. そもそもモデルが対象かどうか。 対象外のモデルで同じ症状が出ているなら、原因はこの変更ではありません。/model で現在のモデルを確認するのが最初の一手です。
2. どの実行面で見ているか。 ここが今回いちばん引っかかった部分でした。バックグラウンドのセッションと Claude Code on the web では、モデルにかかわらず5つとも提供されます。私の手元で「対話セッションでは消えているのに定期実行では並んでいた」のは、まさにこの差です。ローカルのターミナルだけで再現を試みても、web 側の挙動とは一致しません。
3. サブエージェントの側から見えているか。 サブエージェントがこれらのツールを持てるかどうかは、サブエージェント自身のモデルではなく、親セッションが持っているかどうか で決まります。子に Opus 4.7 を指定しても、親が Sonnet 5 で読み込んでいなければ子も持ちません。
3点目には続きがあります。エージェントチームを使っている場合、同じプロセス内で動くチームメイトは親セッションに追随しますが、独立したペインで動くチームメイトは別の Claude Code プロセスなので、そのメイト自身のモデルで決まります。つまり同じチームの中に、共有タスクリストが見えるメンバーと見えないメンバーが混在しうる わけです。共有タスクリストを持たないエージェントは、メッセージのやり取りで足並みを揃えることになります。
チーム構成で「片方だけタスクを拾わない」と感じたら、指示の書き方ではなくこの境界を疑ってください。私はここで小一時間を使いました。
この3点は、頭で辿るよりセッションに直接聞いた方が早く済みます。「今どのツールを持っていますか」と尋ねれば現在の構成が返ってきます。モデル名と実行面から推測するより確実です。今は先に聞いて、後から理由を考える順に変えました。
なお --tools で戻す場合は注意が必要です。この指定はセッションの組み込みツールを列挙したものだけに絞ります。TaskCreate を戻したくて書いたら Edit や Bash が消えていた、という事故が起こり得ます。1つだけ足したいときは --allowedTools の方が安全です。
私が変数を戻さず、台帳を置いた理由
切り分けが済んだあと、素直に CLAUDE_CODE_ENABLE_TODO_TOOLS=1 を書き足すこともできました。実際、途中で人が横から介入する作業では戻す価値があります。
ただ、私が任せている定期メンテナンスは、走り出したら基本的に見ていません。後から見返すのは「何をやると宣言したか」ではなく「結局どのファイルが変わったのか」です。チェックリストは着手前の宣言であって、完了の証拠ではありません。
以前 Claude Code の TodoList はタスク粒度で品質が決まる で、項目は検証可能な単位で切るべきだと書きました。今回はその考えを一段進めて、宣言そのものを外部に持たせるのをやめ、実際に起きたことだけを記録する 方針に変えています。
置いたのは PostToolUse フックで追記する作業台帳です。ファイルを変更するツールが走るたびに1行を足していきます。
#!/usr/bin/env bash
# ~/.claude/hooks/record-change.sh
# PostToolUse フック(matcher: Edit|Write|NotebookEdit)
set -euo pipefail
LEDGER_DIR = "${ CLAUDE_LEDGER_DIR :- $HOME / . claude / ledger }"
mkdir -p " $LEDGER_DIR "
# jq は1回だけ呼ぶ。file_path も notebook_path も無い呼び出しは select で落とす。
# フックの失敗でセッションを止めないため、最後は必ず exit 0 で抜ける。
jq -c --arg ts "$( date -u +%Y-%m-%dT%H:%M:%SZ)" '
(.tool_input.file_path // .tool_input.notebook_path // "") as $f
| select($f != "")
| {ts: $ts,
session: (.session_id // "unknown"),
agent: (.agent_id // "main"),
tool: (.tool_name // "unknown"),
file: $f}
' >> " $LEDGER_DIR /$( date -u +%Y-%m-%d).jsonl" 2> /dev/null || true
exit 0
設定側は次のとおりです。
{
"hooks" : {
"PostToolUse" : [
{
"matcher" : "Edit|Write|NotebookEdit" ,
"hooks" : [
{ "type" : "command" , "command" : "~/.claude/hooks/record-change.sh" }
]
}
]
}
}
agent_id を入れているのは、サブエージェントが並列で動く構成では「誰が触ったか」が後で効くからです。フックがサブエージェント内で発火した場合に限り、この項目が入ってきます。
最初に書いた版では、session_id・agent_id・tool_name・file_path をそれぞれ別々の jq で取り出していました。手元の環境(jq 1.6)で200回続けて呼んで平均を取ると1回あたり95ミリ秒。上のように1回にまとめた版では28ミリ秒で、およそ3.4倍の短縮でした。1回の差は小さく見えますが、フックはファイルを変更するたびに走ります。数百回の編集が走る作業では、この差がそのまま体感の重さになります。
2>/dev/null || true を付けているのは意図的です。壊れた JSON が渡ってきたとき、jq はパースエラーを標準エラーに吐きます。フック自体は終了コード0で抜けるので作業は止まりませんが、ターミナルにエラーが混ざると本来の出力が読みにくくなります。
8並列にしたら、台帳が静かに壊れました
ここからが、事前の予想と逆だった部分です。
追記だけの単純なログなので、並列で書いても壊れないと思っていました。実際、短い行では壊れません。8個のプロセスから60回ずつ、合計480行を同時に追記した結果は480行すべて有効な JSON で、1行あたり平均163バイトでした。
問題は行が長くなったときです。深い階層のパスを持つプロジェクトを想定して、1行が5,293バイトになる条件で同じことをしました。8並列で合計320行。行数はきっちり320行ありました。ところが jq に通してみると、有効な JSON として読めたのは232行だけでした。88行、率にして27.5%が別の行と混ざって壊れていたのです。
行数だけ見れば正常に見えるのが厄介なところです。壊れた記録は、必要になったときに初めて役に立たないことが分かります。
境界は追記の1回の書き込みサイズにあります。おおむね4KiB を超えたあたりから、別プロセスの書き込みが割り込むようになります。並列度を上げること自体は問題ではなく、1行の長さが効きます。サブエージェントのフォークが既定で背景実行になった今、同じ台帳に複数の書き手がいる構成は珍しくありません。
対処は明快で、追記の前後をロックで囲みます。
#!/usr/bin/env bash
# 長い行でも割り込まれない版
set -euo pipefail
LEDGER_DIR = "${ CLAUDE_LEDGER_DIR :- $HOME / . claude / ledger }"
mkdir -p " $LEDGER_DIR "
OUT = " $LEDGER_DIR /$( date -u +%Y-%m-%d).jsonl"
# 9> でロックファイルを開き、flock で追記の全体を保護する。
# ブロック全体を { } で囲むのが要点。jq だけを囲むとリダイレクトが外に出てしまう。
{
flock 9
jq -c --arg ts "$( date -u +%Y-%m-%dT%H:%M:%SZ)" '
(.tool_input.file_path // .tool_input.notebook_path // "") as $f
| select($f != "")
| {ts: $ts,
session: (.session_id // "unknown"),
agent: (.agent_id // "main"),
tool: (.tool_name // "unknown"),
file: $f}
' >> " $OUT " 2> /dev/null || true
} 9> " $LEDGER_DIR /.lock"
exit 0
同じ条件(8並列・5,293バイト・320行)で試したところ、320行すべてが有効な JSON になりました。短い行での負荷も測り直しましたが、1回あたり28ミリ秒が30ミリ秒になった程度です。2ミリ秒で静かな破損を防げるなら、迷う理由はありませんでした。
版 1回あたり 8並列・約5.3KB/行での有効行
jq を4回呼ぶ初版 95 ms 未計測(同一構造のため下段と同じ破損が起きる)
jq 1回・ロックなし 28 ms 320行中 232行
jq 1回・flock あり 30 ms 320行中 320行
数値は手元の Linux 環境(jq 1.6)で測ったものです。絶対値は環境で動きますが、行が長くなると壊れ始めるという性質そのものは変わりません。
台帳を読む側を用意して、はじめて意味が出る
書き溜めるだけでは使えません。ファイル単位でまとめ、最後に触れた順に並べる読み手を用意しました。
#!/usr/bin/env bash
# ~/.claude/hooks/ledger.sh [YYYY-MM-DD]
set -euo pipefail
LEDGER_DIR = "${ CLAUDE_LEDGER_DIR :- $HOME / . claude / ledger }"
DAY = " ${1 :- $( date -u + % Y- % m- % d ) } "
FILE = " $LEDGER_DIR / $DAY .jsonl"
[ -f " $FILE " ] || { echo "no ledger for $DAY " ; exit 0 ; }
jq -rs '
group_by(.file)
| map({
file: .[0].file,
edits: length,
last: (map(.ts) | max),
agents: (map(.agent) | unique | join(","))
})
| sort_by(.last)
| .[]
| "\(.last) \(.edits)x \(.agents) \(.file)"
' " $FILE "
出力はこうなります。
2026-08-16T03:07:47Z 2x main /repo/src/api/client.ts
2026-08-16T03:07:47Z 1x ag-7 /repo/src/api/retry.ts
同じファイルに何度も戻っている箇所は、たいてい迷いが出た場所です。チェックリストの「完了」印には表れませんが、台帳では編集回数として素直に出ます。私はここを見て、あとから自分でレビューする範囲を決めています。
読み手を先に決めておくと、書き手側に何を入れるかも定まります。逆にすると、記録に何でも詰め込んで行が長くなり、先ほどの破損に近づいていきます。
戻すか、置き換えるか
どちらが正解ということはなく、作業の性質で分かれると考えています。台帳を選ぶ場合と変数で戻す場合、私自身の使い分けは次のとおりです。
環境変数で戻した方がよい場合
人が横で見ていて、途中で方針を変える可能性がある作業
エージェントチームで共有タスクリストを前提に分担している場合。持たないエージェントはメッセージで調整することになり、手数が増えます
手順そのものを他の人に説明する必要がある作業
戻さず台帳に寄せた方がよい場合
走り出したら見ていない定期実行。宣言よりも結果が要る
コンテキストの余裕が乏しい長時間の作業。ツール定義とリマインダーの分を丸ごと空けられます
後から「何が変わったか」を機械的に突き合わせたい作業
セッション単位で切り替えられるのも利点です。全体に効かせたくなければ、claude --allowedTools TaskCreate のように起動時に名前を挙げるだけでも読み込まれます。
チームで使う場合は、どちらかに揃えて明文化しておくことをお勧めします。共有タスクリストを持つメンバーと持たないメンバーが混在した状態は、片側からは割り当てたつもりで、もう片側からは見えていないという食い違いを生みます。どちらの向きに揃えるにせよ、二度診断するより安く済みます。
私はしばらく、対話セッションは戻さないまま、台帳だけを常時走らせています。チェックリストが消えて最初は落ち着かなかったのですが、実際に困ったのは初日だけでした。結果を見る習慣がつくと、宣言を眺めていた時間がそれほど役に立っていなかったことに気づきます。
まずは PostToolUse に上のフックを1本だけ入れて、いつもの作業を1回走らせてみてください。その日の台帳を眺めると、自分がどのファイルに何度も戻っているかが分かります。そこが、次に整理すべき場所です。