Leaves out a task reminder that repeats, word for word, the last one the same loop was sent. The model already has that copy in its context; the repeat only…

Leaves out a task reminder that repeats, word for word, the last one the same loop was sent.
A Claude Code mod. Every few turns in which no task tool ran, Claude Code injects a task reminder ("The task tools haven't been used recently ..."), followed by the current task list when there is one. The engine sends it again and again whether or not anything changed, and each copy stays in the context until the next compaction, which it brings sooner.
reminder-diet lets the engine render each task reminder, then compares that text with the last task reminder the same loop (the main conversation, or one subagent) was sent:
/clear: the loop's comparison starts over, so the next reminder goes through, because the earlier copy is no longer in context.Every other injected message (instructions, listings, mentioned files, the background-session nudges) passes through untouched. The transcript keeps the engine's record of every reminder; only the request leaves the repeat out.
In this harness's transcripts over the 14 days to 2026-10-04 there were 27,432 stored task reminders across 3,509 loops. 19,794 of them (72%) carried a task list identical to the previous reminder in the same loop: about 241M characters, roughly 69M tokens of context at 3.5 characters a token, most of it in long-running loops (one loop was sent 995). That count compares the stored rows (the task list the engine renders from), not the rendered text, so the mod's own drop rate is what live use will show.
The saving is context, not price: the reminders sit in the cached prefix, so each repeat costs a cache write once and a cache read on every later request, and more to the point it fills the window and brings compaction sooner.
How it is judged: TaskCreate and TaskUpdate calls per loop over the 14 days after wiring, against the 14 days before. A drop would mean the repeated nudge was doing work, and the mod comes back out: drop its path from CLAUDE_CODE_PLUGIN_DIRS in settings.json. As with the other mods, the operator's verdict from real use decides; there is no fixed threshold.
claude plugin validate reports./register.ts hooks: prompt.attachment, session.compact, prompt.context
./register.ts calls: nothing on $
prompt.attachment once per attachment and keeps the answer for the process, so a reminder left out stays out of every later request and the prompt cache is not disturbed.task_reminder (this build's transcripts) and todo_reminder (the API's docs). A build that renames the reminder again makes the mod a no-op until the name is added.$.ui.invalidate("prompt.attachment") mid-session would re-ask with the latest text already remembered, and then every copy of an unchanged reminder, the first included, would be left out until the list changed. No mod wired in this harness calls it./rewind to before a reminder leaves the mod remembering a copy the conversation no longer holds, so every identical reminder after it is left out until the task list changes or the loop compacts or clears. The engine gives a mod no rewind signal; rewinding is something a person does at the terminal, and what it costs is the nag, not the task list, which reappears with its next change./clear signal is the engine computing the context blocks again (prompt.context), which carries no loop id. Main starts over on every such computation; if it also fires for subagent conversations, main only sends an extra reminder, never fewer.claude --plugin-dir ./reminder-diet
The harness loads it in every session through CLAUDE_CODE_PLUGIN_DIRS (see ../README.md).
claude plugin validate ./reminder-diet
claude plugin test ./reminder-diethooks/register.ts 76 lines1import type { Register } from 'claude-code'
2
3/**
4 * reminder-diet: a task reminder that repeats, word for word, the last one the
5 * same loop was sent is left out of the request.
6 *
7 * Every few turns in which no task tool ran, Claude Code injects a task
8 * reminder ("The task tools haven't been used recently ..."), followed by the
9 * current task list when there is one. In this harness's transcripts over the
10 * 14 days to 2026-10-04, 19,794 of 27,432 stored task reminders (72%) carried a
11 * task list identical to the previous reminder in the same loop: about 241M
12 * characters, most of them in long-running loops (one loop got 995). A repeat
13 * tells the model nothing the earlier copy in its context does not, and it
14 * stays there until the next compaction, which it brings sooner.
15 *
16 * The hook lets the engine, and any plugin beneath, render the reminder first,
17 * then compares that text with the last task reminder the same loop was sent:
18 * equal, it answers null and the request leaves the reminder out; different,
19 * or the loop's first, it passes through and becomes the one compared against.
20 * The engine asks once per attachment and keeps the answer for the process, so
21 * a reminder left out stays out of every later request and the prompt cache is
22 * not disturbed. The transcript keeps the engine's record either way.
23 *
24 * The comparison starts over where the model's context loses the earlier copy:
25 * a loop's own compaction, for the main conversation a fresh context
26 * (`/clear`, which the engine marks by computing the context blocks again),
27 * and for every loop a resume (`session.end` with reason `resume`), since the
28 * resumed conversation's context never held the ended one's reminders.
29 */
30
31/** The names the engine gives its task reminder: `task_reminder` in this build's transcripts, `todo_reminder` in the API's docs. */
32export const REMINDER_TYPES: ReadonlySet<string> = new Set(['task_reminder', 'todo_reminder'])
33/** The key the main conversation is held under; a subagent's loop is held under its agentId. */
34export const MAIN = 'main'
35/** Loops remembered at once. Past this the least recently reminded is forgotten, and its next reminder passes through. */
36export const MAX_LOOPS = 256
37
38export const register: Register = (on) => {
39 /** The text of the last task reminder each loop was sent. Module-level: a reload starts clean, which only lets one repeat through. */
40 const last = new Map<string, string>()
41
42 const remember = (loop: string, text: string) => {
43 last.delete(loop) // re-inserted, the loop moves to the newest end
44 last.set(loop, text)
45 if (last.size > MAX_LOOPS) last.delete(last.keys().next().value as string)
46 }
47
48 on('prompt.attachment', async (_$, e, next) => {
49 if (!REMINDER_TYPES.has(e.type)) return next(e)
50 const answer = await next(e)
51 if (answer.text === null) return answer // already left out beneath: not sent, so not remembered
52 const loop = e.agentId ?? MAIN
53 const repeat = last.get(loop) === answer.text
54 remember(loop, answer.text) // a repeat is still a reminder: the loop moves to the newest end either way
55 return repeat ? { text: null } : answer
56 })
57
58 on('session.compact', async (_$, e, next) => {
59 const result = await next(e)
60 // A precompute only prepares a summary, and a skip keeps the conversation: the earlier copy is still there.
61 // A skip may name `messages` as undefined, so the test is the value, not the key.
62 if (e.trigger !== 'precompute' && result.messages !== undefined) last.delete(e.agentId ?? MAIN)
63 return result
64 })
65
66 on('prompt.context', async (_$, e, next) => {
67 last.delete(MAIN)
68 return next(e)
69 })
70
71 on('session.end', async (_$, e, next) => {
72 if (e.reason === 'resume') last.clear() // another conversation takes this one's place; the module and its map go on
73 return next(e)
74 })
75}
76