Completed tasks are silently deleted from persistent CLAUDE_CODE_TASK_LIST_ID task lists
Summary
Claude Code intentionally garbage-collects completed tasks ("Completed background agents now stay listed in /tasks until cleanup" — v2.1.208 changelog), but this cleanup also deletes completed tasks from persistent, named CLAUDE_CODE_TASK_LIST_ID lists, silently and with an undocumented trigger. For cross-session workflows the completed records are the deliverable (validation/review steps read them), so this is silent data loss: every TaskCreate/TaskUpdate returns success, TaskList shows the board, then the files are deleted with no error surfaced to the model or user.
Environment
- Claude Code 2.1.211, macOS (Darwin 25.5.0), APFS, local ~/.claude/tasks
- Persistent list bound via
CLAUDE_CODE_TASK_LIST_IDin .claude/settings.local.json
Observed behavior (from a real incident, decoded from the session transcript)
An interactive session created 10 tasks over ~2h in ~/.claude/tasks/<list-id>/. Timeline (UTC):
- 01:52–01:54 — tasks 1–4 created (with blockedBy edges)
- 02:05–02:51 — tasks executed and marked completed one by one; a completed task survived 46+ minutes at this stage (TaskList at 02:51:10 shows all 4 completed tasks present)
- 02:52–03:07 — tasks 1–4's N.json files deleted from disk
- 03:07:14 — task 5 marked completed; TaskList at 03:07:16 shows only task 5; TaskList at 03:07:48 shows "No tasks found" (deleted within 34s of completing; no subagent, no user turn, no compaction between the two TaskLists)
- 03:08–03:09 — model re-creates all 5 tasks (ids 6–10), marking each completed; all deleted again within seconds; .highwatermark stamped "10" at deletion time
Deletion fingerprint: every N.json for a completed task is removed; pending/in_progress tasks survive; .lock survives; .highwatermark survives and its mtime matches the deletion (it is written by the cleanup path).
A second flavor: after ~13 sessions started simultaneously (terminal-multiplexer workspace reopen), four different persistent lists — each containing only completed tasks (3, 26, 3, 24 tasks) — went to 0 files within a 2-second window, ~60–70s after session start.
Controlled experiments
- Writes are debounced/async: TaskCreate returned success up to 51s before the file appeared on disk — so failures/cleanups can never surface to the tool call.
- Headless
-psessions never trigger the cleanup (5 attempts: idle sessions, all-completed boards, owner-tagged tasks, subagent-adjacent completion). The cleanup only occurred in long interactive sessions and shortly after session start/resume on all-completed boards. - Two live sessions sharing one list id write cleanly (correct id allocation, no cross-deletion) — plain concurrency is not the cause.
Expected behavior
One or more of:
- Named (non-session-scoped) task lists opt out of completed-task cleanup — persistence is their purpose.
- The cleanup trigger and retention policy are documented and configurable.
- Cleanup is surfaced to the model (e.g., TaskList indicating "N completed tasks archived") instead of silently deleting files that TaskCreate/TaskUpdate just confirmed.
Related (not duplicates)
- v2.1.208 changelog ("stay listed in /tasks until cleanup") — confirms intentional cleanup, trigger undocumented
- #51779 (cleanupPeriodDays covers ~/.claude/tasks — 30-day age-based; too slow to explain this)
- #25298 (tasks not surfacing after compaction — different direction)
- #59962 (task lifecycle state sync)
A second, separable issue found in the same investigation: an ancestor project's .claude/settings.local.json env overrides both a nested worktree's own settings.local.json and the explicit process environment for CLAUDE_CODE_TASK_LIST_ID, silently rebinding worktree sessions to the parent's shared list. Happy to file separately if useful.