A todo list Claude keeps for multi-step work, shown above the prompt

Custom slash commands for Claude Code.
/th1nkful:catchup — Git branch context recovery and plan kickoff/th1nkful:assess — Code review using focused lenses (simplicity, correctness, patterns, tests)/th1nkful:resolve-pr-feedback — Triage and fix unresolved PR review comments/th1nkful:codex-review — Antagonistic code review via codex review, with triage of findings/th1nkful:opencode-code-review — Antagonistic code review via opencode (OpenAI by default), with triage of findings/th1nkful:opencode-plan-review — Antagonistic plan review via opencode (OpenAI by default), with triage of findings/th1nkful:resolve-rebase-conflicts — Safely resolve git rebase conflicts with stacked diff awareness/th1nkful:grill-me — Relentless interview on a plan/design, resolving each branch of the decision tree/th1nkful:simplify — Parallel reuse/quality/efficiency review of changes, then fix the findings/th1nkful:explain-diff-html — Rich, interactive HTML explanation of a code change, diff, branch, or PR (source)# Install all skills
./install.sh
# Install a specific skill
./install.sh codex-review
Skills are installed to ~/.claude/skills/ where Claude Code picks them up as global slash commands.
Re-run ./install.sh after pulling updates.
todos — A todo list Claude keeps for multi-step work, drawn above the prompt. /todos hides/shows it, /todos clear empties it.Mods install as plugins from this repo's marketplace. At the prompt of a terminal session:
/plugin install todos --marketplace th1nkful/claude-skills
Or from a shell:
claude plugin install todos --marketplace th1nkful/claude-skillshooks/register.tsx 106 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Todo } from '../types'
5
6const TOOL = 'mcp__todos__write'
7const list = atom({ plugin: 'todos', key: 'list' } as const, [], { shape: 'todos-1' })
8const isHidden = atom({ plugin: 'todos', key: 'isHidden' } as const, false)
9
10const MARK = { pending: '☐', in_progress: '◐', completed: '☒' } as const
11
12const DESCRIPTION = `Create and update a todo list for the current task, shown to the user above the prompt.
13Use it for work with 3 or more distinct steps, or when the user gives several tasks. Skip it for trivial, single-step work.
14Each call replaces the whole list. Keep exactly one item in_progress while working; mark an item completed as soon as it is done, not in a batch at the end.`
15
16const isTodo = (value: unknown): value is Todo => {
17 const todo = value as Todo
18 return typeof todo?.content === 'string' && todo.status in MARK
19}
20
21const summary = (todos: Todo[]) => {
22 const done = todos.filter(todo => todo.status === 'completed').length
23 return `${done}/${todos.length} done`
24}
25
26export const register: Register = on => {
27 on('session.start', async ($, e, next) => {
28 await $.tool.register({
29 name: 'write',
30 description: DESCRIPTION,
31 isDeferred: false,
32 inputSchema: {
33 type: 'object',
34 properties: {
35 todos: {
36 type: 'array',
37 items: {
38 type: 'object',
39 properties: {
40 content: { type: 'string' },
41 status: { type: 'string', enum: ['pending', 'in_progress', 'completed'] },
42 },
43 required: ['content', 'status'],
44 },
45 },
46 },
47 required: ['todos'],
48 },
49 })
50 await $.command.register({
51 name: 'todos',
52 description: 'Show or hide the todo list; "/todos clear" empties it',
53 argumentHint: '[clear]',
54 })
55
56 return next(e)
57 })
58
59 on('tool.call', { tool: TOOL }, async ($, e) => {
60 const todos = (e as unknown as { todos?: unknown }).todos
61 if (!Array.isArray(todos) || !todos.every(isTodo)) {
62 return { deny: 'todos must be an array of { content, status }.' }
63 }
64
65 await update($, list, () => todos)
66 await update($, isHidden, () => false)
67
68 return { result: `Todo list updated (${summary(todos)}).` }
69 }).catch(() => ({ deny: 'The todo list could not be updated.' }))
70
71 on('command.run', { command: 'todos' }, async ($, e) => {
72 if (e.args.trim() === 'clear') {
73 await update($, list, () => [])
74 return { text: 'Todo list cleared.' }
75 }
76
77 const hidden = await update($, isHidden, value => !value)
78 return { text: hidden ? 'Todo list hidden.' : 'Todo list shown.' }
79 })
80
81 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
82 const todos = await read($, list)
83 if (e.props.hasSurvey || todos.length === 0 || (await read($, isHidden))) {
84 return next(e)
85 }
86
87 const { Box, Text } = $.ui.resolve(e)
88
89 return (
90 <Box flexDirection="column" marginTop={1} paddingLeft={2}>
91 <Text dimColor>TODO · {summary(todos)}</Text>
92 {todos.map(todo => (
93 <Text
94 dimColor={todo.status === 'completed'}
95 strikethrough={todo.status === 'completed'}
96 bold={todo.status === 'in_progress'}
97 color={todo.status === 'in_progress' ? 'claude' : undefined}
98 >
99 {MARK[todo.status]} {todo.content}
100 </Text>
101 ))}
102 </Box>
103 )
104 })
105}
106types/index.d.ts 9 lines1export type TodoStatus = 'pending' | 'in_progress' | 'completed'
2export type Todo = { content: string; status: TodoStatus }
3
4declare module 'claude-code' {
5 interface PluginState {
6 todos: { list: Shaped<Todo[]>; isHidden: boolean }
7 }
8}
9