To-do pane: Claude's live plan, your own to-dos, and an inbox of tasks you dispatch to background subagents in one press.

Claude Code mods (hooks-module plugins) for subagent visibility, task tracking, token savings and daily workflow. Needs Claude Code 2.1.304 or newer (built and tested on 2.1.304; the mod API is early access and can change between releases).
At the prompt of a Claude Code terminal session:
/plugin install agent-monitor --marketplace samarthsh-git/claude-mods
/plugin install task-inbox --marketplace samarthsh-git/claude-mods
/plugin install token-saver --marketplace samarthsh-git/claude-mods
/plugin install daily-driver --marketplace samarthsh-git/claude-mods
/plugin install clarify-first --marketplace samarthsh-git/claude-mods
/plugin install mod-scout --marketplace samarthsh-git/claude-mods
Answer y to add the marketplace, then pick the user scope. From a shell: claude plugin install <mod> --marketplace samarthsh-git/claude-mods.
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with samarthsh-git/claude-mods, then Add plugin.
If this repository is private, the machine needs a git credential for github.com (gh auth login, then gh auth setup-git); the install never prompts for one.
/subagents) that opens by itself when the first subagent starts.$ npm test, reading auth.ts, searching "TODO")./subagents clear wipes the history.⚙ N subagents running, and a toast appears when one fails.The Tasks pane (/todo or /inbox) has three parts:
/todo <text> adds, /todo done 2 ticks, /todo rm 2 removes, /todo clear drops finished ones./inbox <text>, then press Send (or /inbox send). Claude spawns one background subagent per task, in parallel, on the cheapest model that fits. You keep working while they run, and agent-monitor shows their progress.| Feature | What it does | Setting |
|---|---|---|
| Subagent model routing | balanced: search and lookup subagents (Explore, guides) run on Haiku. aggressive: general-purpose and Plan subagents spawned from an Opus-class session also run on Sonnet. A model Claude or you set explicitly is never changed, and neither are forks or plugin agents. | routing |
| Lean-output rules | Adds about 150 tokens to the system prompt: no preamble or recaps, locate before reading, batch tool calls, delegate broad searches, prefer Edit, never trade correctness for brevity. It sits in the cached part of the prompt. | leanRules |
| Duplicate-read guard | Refuses once a Read of a file Claude already read with the same range in the same context while the file is unchanged on disk. Repeating the exact call goes through, so the worst case is one extra round trip. It resets on compaction. | dedupeReads |
| Usage HUD | Status line: ctx 42% · $1.23 · 5h 37%. Toasts at 70% context (suggests /compact or /clear) and at 80% of a rate limit. | always on |
| Model advisor | A toast when a prompt looks trivial for an Opus-class model, or hard for Haiku. Pure regex, costs no tokens, at most once per 15 min. | advisor |
/tokens | Report: fresh, cached and output tokens, cache hit rate, subagent share, what token-saver saved, and concrete tips. |
Change settings with /config (or pluginConfigs in settings.json).
What saves the most, from experience: /clear between unrelated tasks, keeping the prompt cache warm (no model switches mid-thread), delegating read-heavy searches to subagents, and using Sonnet as the default with Opus only for hard problems. The mod nudges toward these; it cannot measure your exact savings.
/aim <goal> sets a focus goal (/aim auto <goal> sets one only when none is set), shown above the prompt with time spent and today's file and commit counts. Press Done or run /aim done to log it.notifyAfterSeconds), you get a desktop or phone notification with Claude's answer. Hand Claude long work and switch tasks./eod (or /eod yesterday) prints a zero-token work log: Claude time, prompts, files changed, commits, test runs, subagents and goals done. Paste it into a standup./handoff has Claude write .claude/handoff.md (goal, done, state, next steps, gotchas). /pickup in the next session continues from it, and a toast reminds you when one exists.On a complex task or a deep, open-ended question, Claude first checks the repo and the chat for what it can learn itself. If something that would change the result is still missing, it asks you up to 4 multiple-choice questions in one round, with a recommended option on each. It then states the agreed spec in 3 lines or fewer, does the work, and checks the result against that spec. Everyday prompts ("fix the typo", "what is X") pass straight through.
Cost:
detector to heuristic for zero detection tokens.Commands: /clarify on (default) · off · always · now <task> (force questions for one task) · /clarify alone shows status. Settings: detector (hybrid / heuristic / model), maxQuestions (1 to 4).
It reads each prompt locally, with no model call and zero tokens, and never changes what Claude sees:
/tokens, a list of 3 or more tasks suggests /inbox, and "wrapping up for today" suggests /handoff. It knows every mod here plus Podium and Grounded Engineering, and stays quiet about mods you have not installed./aim) from your first real task of the session, unless you already set one./run-test, say). This costs no tokens to make or to use beyond the prompt itself. /scout rm <name> removes one./scout build <idea> has Claude write, validate and test a new mod, with zero run-time token cost wherever the feature allows. This is the one part that spends tokens, and only when you ask./scout shows recent suggestions and your macros. /scout off pauses it. Settings: autoMacros, autoFocus.
The mods above run only in Claude Code: the chat app has no plugin system. For chat, chat/ holds the closest equivalents, and both work on every device once set up:
chat/preferences.txt: paste it into Settings → Profile → personal preferences. It holds the lean-output rules, clarifying questions on complex tasks only, and a model-fit hint. It is about 200 words, shorter than a typical preferences block, and preferences ride on every message, so shorter is cheaper.chat/deep-work-skill.zip: upload it in Settings → Capabilities → Skills. It is a gather → ask → spec → deliver → self-check procedure that Claude loads only for complex work, so everyday chats pay just its one-line description.Chat habits that save the most: start a new chat per topic (each message resends the whole chat), put reference files in a Project instead of pasting them again, and use a lighter model for simple asks.
.claude/settings.json registers this marketplace and enables the six mods, so a Claude Code session opened on this repository (from claude.ai/code or the phone app) offers to install them with no commands typed.
These are other people's projects. Install them from their own repositories:
| Mod | What it is | Install |
|---|---|---|
| Podium | Hands jobs to other agent CLIs (Codex, Gemini, other Claude models) and tracks them in one pane, with sprints and PRs. Needs Node 22+, git 2.28+ and at least one other agent CLI. | /plugin marketplace add madjagstudios/plugins, then /plugin install podium@madjagstudios |
| Grounded Engineering | /grounded pane: engineering practices that fit your repo, with sources, plus a reviewed shelf of skill repositories. | /plugin marketplace add madjagstudios/grounded-engineering, then /plugin install grounded-engineering@grounded-engineering |
| claude-ui-mod | Not a Claude Code plugin. It patches the files of the Claude Desktop app on Windows: subagent HUD, task rail, usage meters, provider routing. | Windows only. Quit Claude Desktop, then git clone https://gitlab.com/blockshooter/claude-ui-mod && cd claude-ui-mod && npm ci && node scripts/verify-frontend.cjs && node patch.js apply. Read its SECURITY.md first: it rewrites your Claude Desktop install, and app updates can break it. node patch.js revert undoes it. |
The "optimaizr" and "inbox monitor" mods mentioned in the thread had no links. task-inbox and token-saver cover the same ground.
claude plugin validate mods/<mod>
claude plugin test mods/<mod>
To run a working copy, add "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/abs/path/mods/agent-monitor:/abs/path/mods/task-inbox" } to ~/.claude/settings.json, or start with claude --plugin-dir mods/<mod>.
MIT
hooks/register.tsx 300 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { InboxItem, PlanItem, Todo } from '../types'
5
6const PANE = 'task-inbox'
7const TITLE = 'Tasks'
8
9const plan = atom({ plugin: 'task-inbox', key: 'plan' } as const, [])
10const todos = atom({ plugin: 'task-inbox', key: 'todos' } as const, [])
11const inbox = atom({ plugin: 'task-inbox', key: 'inbox' } as const, [])
12
13function newId(now: number): string {
14 return `${now.toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`
15}
16
17function clip(text: string, n: number): string {
18 const one = text.replace(/\s+/g, ' ').trim()
19 return one.length > n ? one.slice(0, n - 1) + '…' : one
20}
21
22// The one prompt that hands the queued tasks to background subagents.
23function dispatchPrompt(items: readonly InboxItem[]): string {
24 const list = items.map((it, i) => `${i + 1}. ${it.text}`).join('\n')
25 return [
26 `Inbox dispatch (${items.length} task${items.length === 1 ? '' : 's'}). Run each task below in its own background subagent`,
27 '(Agent tool, run_in_background: true), all spawned in one message so they run in parallel.',
28 'Give each subagent a self-contained prompt with the context it needs.',
29 'Pick the cheapest model that will do the task well: haiku for search, lookup and summaries;',
30 'sonnet for routine, well-specified edits; leave the model unset for hard design or debugging.',
31 'Do not wait on them; when each finishes, report one line: task, outcome, and anything I must decide.',
32 '',
33 list,
34 ].join('\n')
35}
36
37async function persist($: EngineInterface, project: string) {
38 await $.store.set(`todos:${project}`, await read($, todos))
39 await $.store.set(`inbox:${project}`, await read($, inbox))
40}
41
42async function addTodo($: EngineInterface, project: string, text: string) {
43 const now = await $.clock.now()
44 await update($, todos, list => [...list, { id: newId(now), text, isDone: false, createdAt: now }])
45 await persist($, project)
46}
47
48async function addInbox($: EngineInterface, project: string, text: string) {
49 const now = await $.clock.now()
50 await update($, inbox, list => [...list, { id: newId(now), text, status: 'queued' as const, sentAt: null }])
51 await persist($, project)
52}
53
54async function dispatch($: EngineInterface, project: string, ids: readonly string[] | null): Promise<number> {
55 const list = await read($, inbox)
56 const picked = list.filter(it => it.status === 'queued' && (ids === null || ids.includes(it.id)))
57 if (picked.length === 0) return 0
58 // Submitted on a timer: a prompt cannot be submitted from inside the
59 // command or press that holds the session, only once it has returned.
60 const text = dispatchPrompt(picked)
61 $.clock.after(100, () => void $.prompt.submit({ text }))
62 const now = await $.clock.now()
63 const sent = new Set(picked.map(it => it.id))
64 await update($, inbox, l => l.map(it => (sent.has(it.id) ? { ...it, status: 'sent' as const, sentAt: now } : it)))
65 await persist($, project)
66 return picked.length
67}
68
69async function refreshStatus($: EngineInterface) {
70 const p = await read($, plan)
71 const mine = (await read($, todos)).filter(t => !t.isDone).length
72 const queued = (await read($, inbox)).filter(it => it.status === 'queued').length
73 const parts: string[] = []
74 if (p.length > 0) parts.push(`plan ${p.filter(x => x.status === 'completed').length}/${p.length}`)
75 if (mine > 0) parts.push(`${mine} to-do`)
76 if (queued > 0) parts.push(`${queued} in inbox`)
77 $.ui.status(parts.length > 0 ? `☑ ${parts.join(' · ')}` : undefined)
78}
79
80export const register: Register = on => {
81 let project = ''
82
83 on('session.start', async ($, e, next) => {
84 project = e.cwd
85 await $.command.register({
86 name: 'todo',
87 description: 'Your to-do list: /todo <text> adds, /todo done <n> ticks, /todo opens the Tasks pane',
88 argumentHint: '[text | done <n> | rm <n> | clear]',
89 })
90 await $.command.register({
91 name: 'inbox',
92 description: 'Queue tasks for background subagents: /inbox <text> queues, /inbox send dispatches all',
93 argumentHint: '[text | send | rm <n> | clear]',
94 })
95 const savedTodos = await $.store.get(`todos:${project}`)
96 const savedInbox = await $.store.get(`inbox:${project}`)
97 if (Array.isArray(savedTodos)) await update($, todos, () => savedTodos as Todo[])
98 if (Array.isArray(savedInbox)) await update($, inbox, () => savedInbox as InboxItem[])
99 await refreshStatus($)
100
101 return next(e)
102 })
103
104 on('command.run', { command: 'todo' }, async ($, e) => {
105 const args = e.args.trim()
106 const list = await read($, todos)
107 const open = list.filter(t => !t.isDone)
108 const m = /^(done|rm)\s+(\d+)$/.exec(args)
109 if (args === '') {
110 await $.ui.open({ id: PANE, title: TITLE })
111 return { text: open.length === 0 ? 'No open to-dos.' : open.map((t, i) => `${i + 1}. ${t.text}`).join('\n') }
112 }
113 if (args === 'clear') {
114 await update($, todos, l => l.filter(t => !t.isDone))
115 await persist($, project)
116 await refreshStatus($)
117 return { text: 'Cleared finished to-dos.' }
118 }
119 if (m) {
120 const target = open[Number(m[2]) - 1]
121 if (!target) return { text: `No open to-do #${m[2]}.` }
122 await update($, todos, l =>
123 m[1] === 'rm' ? l.filter(t => t.id !== target.id) : l.map(t => (t.id === target.id ? { ...t, isDone: true } : t)),
124 )
125 await persist($, project)
126 await refreshStatus($)
127 return { text: `${m[1] === 'rm' ? 'Removed' : 'Done'}: ${target.text}` }
128 }
129 await addTodo($, project, args)
130 await refreshStatus($)
131 return { text: `Added to-do #${open.length + 1}: ${args}` }
132 })
133
134 on('command.run', { command: 'inbox' }, async ($, e) => {
135 const args = e.args.trim()
136 const queued = (await read($, inbox)).filter(it => it.status === 'queued')
137 const m = /^rm\s+(\d+)$/.exec(args)
138 if (args === '') {
139 await $.ui.open({ id: PANE, title: TITLE })
140 return { text: queued.length === 0 ? 'Inbox is empty.' : queued.map((it, i) => `${i + 1}. ${it.text}`).join('\n') }
141 }
142 if (args === 'send') {
143 const n = await dispatch($, project, null)
144 await refreshStatus($)
145 return { text: n === 0 ? 'Nothing queued.' : `Dispatching ${n} task${n === 1 ? '' : 's'} to background subagents.` }
146 }
147 if (args === 'clear') {
148 await update($, inbox, l => l.filter(it => it.status === 'queued'))
149 await persist($, project)
150 return { text: 'Cleared sent tasks.' }
151 }
152 if (m) {
153 const target = queued[Number(m[1]) - 1]
154 if (!target) return { text: `No queued task #${m[1]}.` }
155 await update($, inbox, l => l.filter(it => it.id !== target.id))
156 await persist($, project)
157 await refreshStatus($)
158 return { text: `Removed: ${target.text}` }
159 }
160 await addInbox($, project, args)
161 await refreshStatus($)
162 return { text: `Queued #${queued.length + 1}: ${args}. /inbox send dispatches the queue.` }
163 })
164
165 // Mirror Claude's own plan (TodoWrite, or TaskCreate/TaskUpdate) from the main loop.
166 on('tool.call', { tool: 'TodoWrite' }, async ($, e, next) => {
167 const ran = await next(e)
168 if (e.agentId === undefined && ran.deny === undefined && ran.isError !== true) {
169 await update($, plan, () => e.todos.map((t, i): PlanItem => ({ id: String(i), text: t.content, status: t.status })))
170 await refreshStatus($)
171 }
172 return ran
173 }).catch(($, e, next) => next(e))
174
175 on('tool.call', { tool: 'TaskCreate' }, async ($, e, next) => {
176 const ran = await next(e)
177 if (e.agentId === undefined && ran.deny === undefined && ran.isError !== true) {
178 const id = ran.result.task.id
179 await update($, plan, l => [...l.filter(x => x.id !== id), { id, text: e.subject, status: 'pending' as const }])
180 await refreshStatus($)
181 }
182 return ran
183 }).catch(($, e, next) => next(e))
184
185 on('tool.call', { tool: 'TaskUpdate' }, async ($, e, next) => {
186 const ran = await next(e)
187 if (e.agentId === undefined && ran.deny === undefined && ran.isError !== true) {
188 const status = e.status
189 await update($, plan, l =>
190 status === 'deleted'
191 ? l.filter(x => x.id !== e.taskId)
192 : l.map(x => (x.id === e.taskId ? { ...x, text: e.subject ?? x.text, status: status ?? x.status } : x)),
193 )
194 await refreshStatus($)
195 }
196 return ran
197 }).catch(($, e, next) => next(e))
198
199 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
200 const els = $.ui.resolve(e)
201 const { Box, Text, Button } = els
202 const width = Math.max(20, e.props.bodyColumns ?? 40)
203 const p = await read($, plan)
204 const mine = await read($, todos)
205 const box = await read($, inbox)
206 const queued = box.filter(it => it.status === 'queued')
207 const sent = box.filter(it => it.status === 'sent')
208 const mark = { pending: '☐', in_progress: '◐', completed: '☑' } as const
209 const field = (key: string, placeholder: string, onSubmit: (text: string) => Promise<void>) =>
210 'Input' in els ? (
211 <els.Input key={key} placeholder={placeholder} submitLabel="Add" onSubmit={text => void (text.trim() && onSubmit(text.trim()))} />
212 ) : null
213
214 return (
215 <Box flexDirection="column">
216 <Text bold>Claude's plan {p.length > 0 ? `(${p.filter(x => x.status === 'completed').length}/${p.length})` : ''}</Text>
217 {p.length === 0 && <Text dimColor>No plan yet. It fills in as Claude tracks its steps.</Text>}
218 {p.map(x => (
219 <Text key={`p-${x.id}`} color={x.status === 'in_progress' ? 'claude' : undefined} dimColor={x.status === 'completed'} wrap="truncate">
220 {mark[x.status]} {clip(x.text, width - 3)}
221 </Text>
222 ))}
223
224 <Box marginTop={1}>
225 <Text bold>My to-dos ({mine.filter(t => !t.isDone).length})</Text>
226 </Box>
227 {mine.map(t => (
228 <Box key={`t-${t.id}`} flexDirection="row">
229 <Button
230 key={`tt-${t.id}`}
231 plain
232 onPress={async () => {
233 await update($, todos, l => l.map(x => (x.id === t.id ? { ...x, isDone: !x.isDone } : x)))
234 await persist($, project)
235 await refreshStatus($)
236 }}
237 >
238 {t.isDone ? '☑' : '☐'} <Text dimColor={t.isDone} strikethrough={t.isDone}>{clip(t.text, width - 8)}</Text>
239 </Button>
240 <Button
241 key={`tx-${t.id}`}
242 plain
243 dimColor
244 label=" ✕"
245 onPress={async () => {
246 await update($, todos, l => l.filter(x => x.id !== t.id))
247 await persist($, project)
248 await refreshStatus($)
249 }}
250 />
251 </Box>
252 ))}
253 {field('new-todo', 'Add a to-do…', async text => {
254 await addTodo($, project, text)
255 await refreshStatus($)
256 })}
257
258 <Box marginTop={1}>
259 <Text bold>Inbox ({queued.length} queued)</Text>
260 </Box>
261 {queued.length === 0 && <Text dimColor>Queue tasks with /inbox <task>, then send them all at once.</Text>}
262 {queued.map(it => (
263 <Box key={`i-${it.id}`} flexDirection="row">
264 <Text wrap="truncate">• {clip(it.text, width - 14)} </Text>
265 <Button key={`is-${it.id}`} label="Send" onPress={async () => (await dispatch($, project, [it.id]), refreshStatus($))} />
266 <Button
267 key={`ix-${it.id}`}
268 plain
269 dimColor
270 label=" ✕"
271 onPress={async () => {
272 await update($, inbox, l => l.filter(x => x.id !== it.id))
273 await persist($, project)
274 await refreshStatus($)
275 }}
276 />
277 </Box>
278 ))}
279 {field('new-inbox', 'Queue a task for a subagent…', async text => {
280 await addInbox($, project, text)
281 await refreshStatus($)
282 })}
283 {queued.length > 1 && (
284 <Button key="send-all" variant="primary" label={`Send all ${queued.length}`} onPress={async () => (await dispatch($, project, null), refreshStatus($))} />
285 )}
286 {sent.length > 0 && (
287 <Box flexDirection="column" marginTop={1}>
288 <Text dimColor>Sent ({sent.length}): progress shows in /subagents</Text>
289 {sent.slice(-5).map(it => (
290 <Text key={`s-${it.id}`} dimColor wrap="truncate">
291 ↗ {clip(it.text, width - 3)}
292 </Text>
293 ))}
294 </Box>
295 )}
296 </Box>
297 )
298 })
299}
300types/index.d.ts 14 lines1export type PlanStatus = 'pending' | 'in_progress' | 'completed'
2
3export type PlanItem = { id: string; text: string; status: PlanStatus }
4
5export type Todo = { id: string; text: string; isDone: boolean; createdAt: number }
6
7export type InboxItem = { id: string; text: string; status: 'queued' | 'sent'; sentAt: number | null }
8
9declare module 'claude-code' {
10 interface PluginState {
11 'task-inbox': { plan: PlanItem[]; todos: Todo[]; inbox: InboxItem[] }
12 }
13}
14