Your own persistent todo list and notes across sessions: /mytodo and /mynote, with an open-count in the status line.

Small, independent mods for Claude Code: a live progress bar, a usage dashboard, a context-window meter, a safety guard, a git status band, and your own notes and todos.
Each mod is a plugin of function hooks. Install only the ones you want; none depends on another.
| Mod | Command | What it gives you |
|---|---|---|
| task-progress | /progress | A progress band above the prompt that follows the todo list, with a working timer and your usage. |
| usage-dashboard | /usage-dashboard | A pane with 5h and weekly limits, models, tasks, time left and a 7-day chart. |
| context-meter | /ctxmeter | Context-window percentage in the status line, with warnings at 70% and 90%. |
| safety-guard | /safeguard | Blocks destructive commands and writes to secret or key files before they run. |
| git-band | /gitband | Branch, ahead/behind and change counts on one line above the prompt. |
| notes-todos | /mytodo, /mynote | Your own todo list and notes that persist across sessions. |
Run these in a terminal Claude Code session. The desktop app's Code tab does not run /plugin install.
/plugin install task-progress --marketplace wablevaibhav/claude-mods
Answer y to add the marketplace the first time, then pick a scope. Repeat with any other mod name from the table:
/plugin install usage-dashboard --marketplace wablevaibhav/claude-mods
/plugin install context-meter --marketplace wablevaibhav/claude-mods
/plugin install safety-guard --marketplace wablevaibhav/claude-mods
/plugin install git-band --marketplace wablevaibhav/claude-mods
/plugin install notes-todos --marketplace wablevaibhav/claude-mods
Clone the repo and point Claude Code at the mod folders:
git clone https://github.com/wablevaibhav/claude-mods.git
cd claude-mods
claude --plugin-dir ./task-progress --plugin-dir ./usage-dashboard
Add one --plugin-dir per mod.
task-progressis a real screenshot. The other previews are illustrative text with sample numbers; real colours (green / amber / red, orange accents) follow your terminal theme.
task-progress: the band above the prompt

usage-dashboard: the /usage-dashboard pane
Claude usage updated 12s ago
╭──────────────────────────────────────────────────────────────────╮
│ 5-hour session resets in 2h 10m │
│ ██████████████▌░░░░░░░░░░░░░░░░░░░░░░░░░ 35% │
│ burning 0.40%/min · full in ~2h 5m │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Weekly resets in 4d 3h │
│ █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12% │
╰──────────────────────────────────────────────────────────────────╯
● Limit risk green session cost ~$1.42
╭──────────────────────────────────────────────────────────────────╮
│ Models active: opus-5-5 (3m ago) │
│ ★ opus-5-5 ████████░░ 78% 1.2M 34 msgs ~$0.91 │
│ sonnet-5-5 ██░░░░░░░░ 22% 340k 12 msgs ~$0.18 │
│ tokens: in / out / cache = 80k / 190k / 1.3M │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Tasks ▸ 1 in progress ○ 3 pending ✓ 2 today │
│ ▸ Wire the charts 1m 20s │
│ ○ Add the tasks board │
│ ○ Polish and test │
│ Time left 25m (best case 14m) · confidence medium, 4 finished │
│ Should finish within the current 5h window. │
╰──────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────╮
│ Last 7 days │
│ Wed 07 ██████████████████░░░░░░░░░░░░░░░░░░░░░░ 420k │
│ Thu 08 ███████████████████████████████░░░░░░░░░ 740k │
│ Fri 09 ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 240k │
│ Sat 10 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0 │
│ Sun 11 ██████████████████████░░░░░░░░░░░░░░░░░░ 530k │
│ Mon 12 ████████████████████████████████████████ 1.0M │
╰──────────────────────────────────────────────────────────────────╯
context-meter and git-band each add one line above the prompt:
▲ Context 78% ███████████████████░░░░░ 156k / 200k Getting full: consider /compact soon. [Hide]
⎇ feature/login ↑2 ↓1 ● 3 staged ✎ 2 changed ? 2 untracked
A progress band above the prompt, visible while you type.
progress tool. Claude can report title, total, done, note, items and finished explicitly (the tool is mcp__task-progress__progress), for work that doesn't use a todo list.⠹ Working 12s) whenever a turn is running and no job is shown.Collapse / Expand hides the step list, Hide or /progress hides the band. A finished job clears when you send your next prompt.Open it with /usage-dashboard. (/usage is Claude Code's own built-in panel; this mod uses a different name so the two don't collide.)
5h 34% · wk 12% · opus-5-5 after your first reply.ctx 43%, the share of the context window in use./ctxmeter hides or shows the band.Stops a call before it runs and tells Claude why, so it asks you first.
| Blocks | Examples | ||
|---|---|---|---|
| Recursive deletes of root, home or wildcards | rm -rf /, rm -r ~, rm -rf *, rm -rf /etc | ||
| Force pushes without a lease | git push --force, git push -f (--force-with-lease is allowed) | ||
| Piping a download into a shell | `curl … \ | sh, wget … \ | sudo bash` |
| Disk and filesystem damage | dd … of=/dev/sda, mkfs.*, > /dev/sda, fork bombs, chmod -R 777 | ||
| Writing secrets | .env, .env.production (not .env.example), *.pem, *.key, id_rsa, service-account JSON, .npmrc, .aws/credentials, .git/ internals | ||
| Reading private keys | id_rsa, *.pem, *.key, .ssh/, .aws/credentials |
/safeguard shows status and how many calls it blocked. /safeguard pause and /safeguard resume turn it off and on for the session. If the guard itself errors, it blocks the call rather than letting it through.
detached HEAD), ↑ahead ↓behind against the upstream, staged, changed and untracked counts, conflicts, or ✓ clean./gitband hides or shows it.Your own lists, separate from Claude's todo list, stored locally and kept across sessions. The open-todo count shows in the status line.
/mytodo add Renew the domain add a todo (a bare /mytodo <text> also adds)
/mytodo list todos
/mytodo done 2 /mytodo undo 2 /mytodo rm 2 /mytodo clear
/mynote Wifi password is on the fridge save a note
/mynote list notes
/mynote find wifi /mynote rm 3
five_hour, seven_day). Accounts without those windows (some API-key setups) show "No reading yet".TodoWrite list. If your session uses another task tool, task-progress can still be driven through its progress tool./usage, /context and /todos.git-band runs git status.| Problem | What to try |
|---|---|
/usage shows "Session limit / Breakdown" | That is Claude Code's built-in panel. Use /usage-dashboard. |
| A mod's command isn't found | The mod isn't loaded. Install it, or start Claude with --plugin-dir ./<mod>. |
| Gauges say "No reading yet" | Send one message; the reading arrives with the first reply. |
| Band shows the old layout after an update | Close the pane and reopen it, or restart Claude. |
| A command was blocked unexpectedly | Run /safeguard to see why, /safeguard pause if you really want it. |
/plugin install isn't available | Use a terminal Claude Code session, not the desktop app's Code tab. |
claude plugin validate . # the marketplace
claude plugin validate ./<mod> # one mod
claude plugin test ./<mod> # its tests
Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json, a hooks module, pure logic with *.test.ts files, and (where it keeps state) a types/index.d.ts contract. The marketplace file is .claude-plugin/marketplace.json.
Releases are git tags: v0.2.0 added context-meter, safety-guard, git-band and notes-todos.
MIT, see LICENSE.
hooks/register.ts 50 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { empty, openCount, runNote, runTodo } from './lists'
4import type { Data } from './lists'
5
6const KEY = 'notes-todos.v1'
7
8async function load($: EngineInterface): Promise<Data> {
9 const saved = (await $.store.get(KEY)) as Data | undefined
10 return saved && Array.isArray(saved.todos) && Array.isArray(saved.notes) ? saved : empty()
11}
12
13async function showCount($: EngineInterface, data: Data) {
14 const open = openCount(data)
15 $.ui.status(open > 0 ? `todo ${open}` : undefined)
16}
17
18export const register: Register = on => {
19 on('session.start', async ($, e, next) => {
20 await $.command.register({
21 name: 'mytodo',
22 description: 'Your own todo list: /mytodo [add <text> | done <id> | undo <id> | rm <id> | clear]',
23 })
24 await $.command.register({
25 name: 'mynote',
26 description: 'Your own notes: /mynote [<text> | rm <id> | find <text>]',
27 })
28 await showCount($, await load($)).catch(() => undefined)
29
30 return next(e)
31 })
32
33 on('command.run', { command: 'mytodo' }, async ($, e) => {
34 const now = await $.clock.now()
35 const { data, text } = runTodo(await load($), e.args, now)
36 await $.store.set(KEY, data)
37 await showCount($, data)
38
39 return { text }
40 }).catch(() => ({ text: 'Todo list unavailable.' }))
41
42 on('command.run', { command: 'mynote' }, async ($, e) => {
43 const now = await $.clock.now()
44 const { data, text } = runNote(await load($), e.args, now)
45 await $.store.set(KEY, data)
46
47 return { text }
48 }).catch(() => ({ text: 'Notes unavailable.' }))
49}
50hooks/lists.ts 95 lines1export type Todo = { id: number; text: string; done: boolean; at: number }
2export type Note = { id: number; text: string; at: number }
3export type Data = { todos: Todo[]; notes: Note[]; nextId: number }
4
5export const empty = (): Data => ({ todos: [], notes: [], nextId: 1 })
6
7export const openCount = (data: Data): number => data.todos.filter(t => !t.done).length
8
9const split = (args: string): { verb: string; rest: string } => {
10 const trimmed = args.trim()
11 const space = trimmed.search(/\s/)
12 return space < 0
13 ? { verb: trimmed.toLowerCase(), rest: '' }
14 : { verb: trimmed.slice(0, space).toLowerCase(), rest: trimmed.slice(space + 1).trim() }
15}
16
17const parseId = (text: string): number | null => {
18 const n = Number(text.replace(/^#/, ''))
19 return Number.isInteger(n) && n > 0 ? n : null
20}
21
22export const renderTodos = (todos: readonly Todo[]): string => {
23 if (todos.length === 0) return 'No todos. Add one with /mytodo add <text>.'
24 const open = todos.filter(t => !t.done)
25 const done = todos.filter(t => t.done)
26 const line = (t: Todo) => `${t.done ? '[x]' : '[ ]'} #${t.id} ${t.text}`
27 return [...open.map(line), ...(done.length > 0 ? ['', ...done.map(line)] : [])].join('\n')
28}
29
30export const renderNotes = (notes: readonly Note[]): string =>
31 notes.length === 0
32 ? 'No notes. Add one with /mynote <text>.'
33 : notes.map(n => `#${n.id} ${n.text}`).join('\n')
34
35/** One /mytodo command: returns the new data and the text to show. */
36export const runTodo = (data: Data, args: string, now: number): { data: Data; text: string } => {
37 const { verb, rest } = split(args)
38
39 if (verb === '' || verb === 'list') return { data, text: renderTodos(data.todos) }
40
41 if (verb === 'done' || verb === 'undo' || verb === 'rm') {
42 const id = parseId(rest)
43 const target = id === null ? undefined : data.todos.find(t => t.id === id)
44 if (id === null || target === undefined) return { data, text: `No todo ${rest || '(missing id)'}.` }
45 if (verb === 'rm') {
46 const next = { ...data, todos: data.todos.filter(t => t.id !== id) }
47 return { data: next, text: `Removed #${id}.\n${renderTodos(next.todos)}` }
48 }
49 const next = {
50 ...data,
51 todos: data.todos.map(t => (t.id === id ? { ...t, done: verb === 'done' } : t)),
52 }
53 return { data: next, text: renderTodos(next.todos) }
54 }
55
56 if (verb === 'clear') {
57 const next = { ...data, todos: data.todos.filter(t => !t.done) }
58 return { data: next, text: `Cleared finished todos.\n${renderTodos(next.todos)}` }
59 }
60
61 const text = verb === 'add' ? rest : args.trim()
62 if (text === '') return { data, text: 'Usage: /mytodo add <text>' }
63 const todo: Todo = { id: data.nextId, text, done: false, at: now }
64 const next = { ...data, todos: [...data.todos, todo], nextId: data.nextId + 1 }
65 return { data: next, text: `Added #${todo.id}.\n${renderTodos(next.todos)}` }
66}
67
68/** One /mynote command. */
69export const runNote = (data: Data, args: string, now: number): { data: Data; text: string } => {
70 const { verb, rest } = split(args)
71
72 if (verb === '' || verb === 'list') return { data, text: renderNotes(data.notes) }
73
74 if (verb === 'rm') {
75 const id = parseId(rest)
76 if (id === null || !data.notes.some(n => n.id === id)) {
77 return { data, text: `No note ${rest || '(missing id)'}.` }
78 }
79 const next = { ...data, notes: data.notes.filter(n => n.id !== id) }
80 return { data: next, text: `Removed #${id}.\n${renderNotes(next.notes)}` }
81 }
82
83 if (verb === 'find') {
84 const q = rest.toLowerCase()
85 const hits = data.notes.filter(n => n.text.toLowerCase().includes(q))
86 return { data, text: q === '' ? 'Usage: /mynote find <text>' : renderNotes(hits) }
87 }
88
89 const text = verb === 'add' ? rest : args.trim()
90 if (text === '') return { data, text: 'Usage: /mynote <text>' }
91 const note: Note = { id: data.nextId, text, at: now }
92 const next = { ...data, notes: [...data.notes, note], nextId: data.nextId + 1 }
93 return { data: next, text: `Saved #${note.id}.` }
94}
95