●SUNSET — The legacy Workbench and experimental prompt tool APIs retire tomorrow, August 17, one day out●LATEST — Version 2.1.233, released August 15, is current: GitLab merge request URLs now work with --worktree and the claude agents view, where MRs appear as !N●SECURITY — Windows paths written with the NT \??\ device prefix no longer bypass UNC validation, closing an NTLM credential-leak vector●TODO — Todo and task tracking tools are off by default on Opus 4.8, Sonnet 5, Fable 5, Mythos 5, and newer models; set CLAUDE_CODE_ENABLE_TODO_TOOLS=1 to bring them back●FORK — Version 2.1.232, released August 14, makes subagent_type: 'fork' the default, so a forked subagent inherits the full conversation and prompt cache●MENTION — Typing @ in the prompt now mentions another Claude session by name, and SendMessage reaches that session directly●SUNSET — The legacy Workbench and experimental prompt tool APIs retire tomorrow, August 17, one day out●LATEST — Version 2.1.233, released August 15, is current: GitLab merge request URLs now work with --worktree and the claude agents view, where MRs appear as !N●SECURITY — Windows paths written with the NT \??\ device prefix no longer bypass UNC validation, closing an NTLM credential-leak vector●TODO — Todo and task tracking tools are off by default on Opus 4.8, Sonnet 5, Fable 5, Mythos 5, and newer models; set CLAUDE_CODE_ENABLE_TODO_TOOLS=1 to bring them back●FORK — Version 2.1.232, released August 14, makes subagent_type: 'fork' the default, so a forked subagent inherits the full conversation and prompt cache●MENTION — Typing @ in the prompt now mentions another Claude session by name, and SendMessage reaches that session directly
Tracking Multi-Step Work in Claude Code Now That the Todo Tools Are Off by Default
Since Claude Code v2.1.233, TodoWrite and the Task tools are left out on the newer models. Here are the three branches to check before you set the opt-in variable, the PostToolUse ledger I put in their place, and the measured point where that ledger quietly corrupts under parallel agents.
I handed Claude Code my usual morning routine. As an indie developer running several apps and sites on my own, the pre-release checks and the recurring site maintenance all go out as fixed multi-step sequences.
That morning the screen looked different. The work was moving along, but the progress checklist that normally stacks up on the left stayed empty the whole way through.
What made it strange was that the same sequence, running on a schedule, still showed its tasks exactly as before. If something had broken, it should have broken in both places. When only one side goes quiet, the cause is usually a changed assumption rather than a changed setting.
What v2.1.233 changed was whether the tools get loaded at all
The cause was not on my side. As of Claude Code v2.1.233, five tools — TodoWrite, TaskCreate, TaskGet, TaskUpdate, and TaskList — are not loaded on Opus 4.8, Sonnet 5, Fable 5, Mythos 5, or later members of those families unless you opt in.
The reasoning is spelled out in the tools reference: those models keep track of multi-step work without a written checklist, and the tool definitions plus their reminders take up context, so Claude Code leaves them out. With the tools absent, nothing gets added to the task list while the work runs.
So it was never a case of tasks failing to appear. The instrument for adding them simply was not in the room. Miss that distinction and you will spend the morning suspecting your prompt or the model's reasoning.
The confusing part is that the same goal — "I want the checklist back" — points at opposite settings depending on the model.
Session model
Loaded by default
Variable to change it
Opus 4.8 / Sonnet 5 / Fable 5 / Mythos 5 and later
None of the five
CLAUDE_CODE_ENABLE_TODO_TOOLS=1 to add them
Anything else, such as Opus 4.7
The four Task tools
CLAUDE_CODE_ENABLE_TASKS=0 to fall back to TodoWrite
One is addition, the other is subtraction. Carry a config file over from an older setup and the two will not line up.
Three branches to check before you set the variable
Setting CLAUDE_CODE_ENABLE_TODO_TOOLS=1 brings all five back on every model and every provider. But if you set it before isolating the cause, you can end up either fixing nothing or papering over a mismatch that resurfaces somewhere else. Here is the order I worked through.
1. Is the model even in scope? If you see the same symptom on a model outside that list, this change is not your cause. Checking /model first takes seconds.
2. Which surface are you looking at? This is where I lost the most time. Background sessions and Claude Code on the web provide all five regardless of model. That is exactly why my interactive session had gone quiet while the scheduled run kept listing tasks. Trying to reproduce the behavior in a local terminal will never match what the web surface does.
3. Are you looking from inside a subagent? Whether a subagent gets these tools depends not on the subagent's own model but on whether your session has them. Point a child at Opus 4.7 and it still comes up empty if the parent runs Sonnet 5 without the tools loaded.
That third point has a tail. With agent teams, an in-process teammate follows your session, but a teammate in its own split pane runs as a separate Claude Code process, so its own model decides. Which means one team can contain members who see the shared task list and members who do not. Agents without the Task tools coordinate through messages instead.
If one member of a team keeps failing to pick up work, look at this boundary before you rewrite the instructions. I spent the better part of an hour on the instructions first.
There is a cheaper way to settle all three questions than reasoning about them: ask the session what it is holding. Typing "what tools do you have access to?" gets you a plain answer about the current set, which is faster than inferring the answer from the model name and the surface. I now do this first and reason second, because the three branches interact — a background run of the same subagent on the same model behaves differently from the interactive one, and no amount of reading the config tells you which case you are in.
One caveat on that check: --tools restricts a session to exactly the built-in tools you name. If you reach for it to bring back TaskCreate and forget to list Edit and Bash alongside it, you get the checklist and lose the ability to do the work. --allowedTools is the safer of the two when all you want is one tool back.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will be able to narrow a missing task list down to configuration, surface, or subagent scope within a few minutes of noticing it
✦You will be able to close the gap where a shared append-only log silently corrupts before parallel agents ever hit it
✦You will be able to decide, from the shape of your own work, whether to restore the checklist or move to an external ledger
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Why I left the variable alone and built a ledger instead
Once the cause was clear, adding CLAUDE_CODE_ENABLE_TODO_TOOLS=1 would have been the obvious move. For work where a person is watching and steering mid-run, it is worth it.
But the maintenance I hand off runs unattended. What I go back to afterwards is never "what did it say it would do" — it is "which files actually changed." A checklist is a statement of intent made before the work. It is not evidence that the work happened.
agent_id is in there because once subagents run in parallel, "who touched this" is the field you end up wanting. It is populated only when the hook fires inside a subagent.
My first version pulled session_id, agent_id, tool_name, and file_path out with four separate jq calls. Two hundred consecutive runs on my machine (jq 1.6) averaged 95 ms each. Folding them into the single call above brought that to 28 ms, roughly a 3.4x reduction. The per-call difference looks trivial until you remember the hook fires on every file change; across a few hundred edits it is the difference you feel.
The 2>/dev/null || true is deliberate. When malformed JSON arrives, jq writes a parse error to stderr. The hook still exits 0 and the session keeps going, but the error text lands in your terminal and buries the output you were reading.
At eight writers in parallel, the ledger corrupted silently
Here is the part that ran against my expectations.
An append-only log felt safe to write from many processes at once. With short lines it is. Eight processes writing 60 lines each — 480 lines at an average of 163 bytes — came out as 480 valid JSON records.
Long lines are a different story. I repeated the run with paths deep enough to push each record to 5,293 bytes: eight writers, 320 lines total. The file had exactly 320 lines. Piping it through jq, only 232 of them parsed. Eighty-eight records, 27.5% of the run, had been interleaved into garbage.
The line count looking correct is what makes this dangerous. You discover the damage at the moment you need the record.
The boundary sits at the size of a single append. Past roughly 4 KiB, another process's write starts landing in the middle of yours. Parallelism itself is not the problem; line length is. And with forked subagents now running in the background by default, multiple writers on one ledger is an ordinary configuration rather than an exotic one.
The fix is to wrap the append in a lock.
#!/usr/bin/env bash# Version that survives lines past PIPE_BUFset -euo pipefailLEDGER_DIR="${CLAUDE_LEDGER_DIR:-$HOME/.claude/ledger}"mkdir -p "$LEDGER_DIR"OUT="$LEDGER_DIR/$(date -u +%Y-%m-%d).jsonl"# 9> opens the lock file; flock holds it across the whole append.# Wrapping the block in { } matters — locking jq alone leaves the# redirect outside the protected region.{ 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
Under the same conditions — eight writers, 5,293 bytes per line, 320 lines — all 320 parsed. I re-measured the short-line cost as well: 28 ms became 30 ms. Two milliseconds to rule out silent corruption is not a difficult trade.
Version
Per call
Valid lines at 8 writers, ~5.3 KB each
Four jq calls
95 ms
Not measured; same structure, same corruption as below
One jq call, no lock
28 ms
232 of 320
One jq call with flock
30 ms
320 of 320
These numbers come from my Linux environment running jq 1.6. The absolute values will move on your machine; the property that long lines start tearing will not.
The ledger only pays off once you build the reader
Accumulating lines is not the point. I wrote a reader that groups by file and orders by the last time each was touched.
2026-08-16T03:07:47Z 2x main /repo/src/api/client.ts2026-08-16T03:07:47Z 1x ag-7 /repo/src/api/retry.ts
A file returned to several times is usually a place where something was unclear. A checklist's "done" marker never shows that; an edit count does, without being asked. That column is what I use to decide which parts of a run I review myself.
Deciding on the reader first also settles what belongs in the writer. Do it the other way round and you pack everything into each record, the lines grow, and you drift toward the corruption above.
Restore, or replace
Neither answer is universally right. It comes down to the shape of the work. Mine splits like this.
Worth restoring the variable
Work where someone is watching and may change direction partway through
Agent teams that divide labor through the shared task list; members without it fall back to messages, which costs turns
Work where you need to walk someone else through the procedure
Better served by the ledger
Unattended scheduled runs, where the result matters and the intent does not
Long sessions with little context to spare; you reclaim the whole tool-definition-and-reminder budget
Work you later want to reconcile mechanically against what changed
You can also decide this per session rather than globally. Naming a tool at launch is enough: claude --allowedTools TaskCreate loads it for that run alone.
If you work in a team, decide once and write it down somewhere the whole team reads. A mixed setup, where some members hold the shared task list and others coordinate by message, is the configuration that produces the confusing failures: work looks assigned from one side and invisible from the other, and nobody's logs disagree. Making it uniform in either direction costs less than diagnosing it twice.
For now I leave my interactive sessions as they are and keep the ledger running everywhere. Losing the checklist felt unsettling at first, but the discomfort lasted exactly one day. Once you get used to reading results, it becomes clear how little the intent was doing for you.
Start with just the PostToolUse hook and one ordinary run. Read that day's ledger and see which files you kept coming back to. That is the part worth cleaning up next.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.