Start a timed sprint on one task with /sprint. Prompts about anything else are held until the timer ends.

Claude Code now has mods. Small add-ons that change how Claude itself looks and behaves. These three turn it from an assistant into something closer to a manager that works for you.
| Mod | What it does |
|---|---|
| Daily Command Center | Every morning from 9am, Claude asks for the three tasks you need to do today and stays locked until you type them. Then it pins them in a side pane next to your revenue and key numbers. |
| Focus Lock | Pick one task, start a timed sprint, and Claude holds any prompt that is not about that task until the timer ends. |
| Done Chime | Plays a sound and shows a toast when Claude finishes a long job, so you can walk away instead of watching the screen. |
Step 1. Update Claude Code. Mods need a recent version. These were built and tested on 2.1.289. Older versions reject them.
Step 2. Open Claude Code and paste this:
Install these mods for me: https://github.com/tenfoldmarc/claude-code-mods
Step 3. Start a new session. That's it.
claude plugin marketplace add tenfoldmarc/claude-code-mods
claude plugin install daily-command-center@tenfold-mods
claude plugin install focus-lock@tenfold-mods
claude plugin install done-chime@tenfold-mods
Install only the ones you want. Then start a new session, or run /reload-plugins in an open one.
1, 2 or 3 in the pane to check one off.| Command | What it does |
|---|---|
/today | Open the Today pane |
/today done 2 | Check off task 2 |
/today reset | Clear today's tasks and get asked again |
/today skip | No tasks today |
/today refresh | Refresh the numbers now |
/metric Followers 12,400 | Pin any number to the pane |
/metric Followers | Remove that number |
Your numbers. Three ways to get them in, all optional:
/metric. Zero setup./plugin configure daily-command-center@tenfold-mods and paste a Stripe key. It shows revenue today and month to date. Use a restricted, read-only key with read access to Balance only, never your full secret key. Create one in Stripe under Developers, API keys, Create restricted key. Claude Code keeps it in your system's secure storage.Label: value line per number. Email subscribers, followers, anything you can fetch.| Command | What it does |
|---|---|
/sprint 50 write the sales page | Start a 50 minute sprint on that task |
/sprint write the sales page | Same, with your default length |
/sprint | See the time left |
/sprint stop | End the sprint early |
!! to push it through anyway.Nothing to do. Any job that runs 20 seconds or longer ends with a chime and a toast showing how long it took. Errors get a different sound.
Run /plugin configure <mod>@tenfold-mods to change these.
| Mod | Setting | Default |
|---|---|---|
| Daily Command Center | Start hour | 9 |
| Daily Command Center | Stripe key | none |
| Daily Command Center | Metrics command | none |
| Daily Command Center | Refresh minutes | 5 |
| Focus Lock | Sprint length (minutes) | 50 |
| Focus Lock | Judge model | haiku |
| Done Chime | Minimum seconds | 20 |
| All three | Volume (0 to 4) | 1 |
claude plugin uninstall <mod>@tenfold-mods.claude -p scripts are never held or captured.Paste this into Claude Code:
Update the mods from https://github.com/tenfoldmarc/claude-code-mods
Or in a terminal:
claude plugin marketplace update tenfold-mods
claude plugin update daily-command-center@tenfold-mods
Repeat the second command for focus-lock and done-chime.
If a user asks you to install these mods, do this:
claude --version. If it is older than 2.1.289, tell the user to update Claude Code first and stop. If claude is not on the PATH, give the user the terminal commands from the Install section to run themselves.claude plugin marketplace add tenfoldmarc/claude-code-mods.claude plugin install for each of daily-command-center@tenfold-mods, focus-lock@tenfold-mods and done-chime@tenfold-mods, unless the user asked for only some of them./reload-plugins), and that after 9am the Daily Command Center will ask for their three tasks./plugin configure daily-command-center@tenfold-mods and a restricted, read-only key.To update: claude plugin marketplace update tenfold-mods, then claude plugin update <mod>@tenfold-mods for each installed mod.
MIT. The sounds are original and included under the same license.
Built by @tenfoldmarc. Follow for daily AI builds. Real systems, not theory.
hooks/register.tsx 283 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Sprint } from '../types'
5
6const sprint = atom({ plugin: 'focus-lock', key: 'sprint' } as const, null)
7const held = atom({ plugin: 'focus-lock', key: 'held' } as const, [])
8const now = atom({ plugin: 'focus-lock', key: 'now' } as const, 0)
9
10const SOUND = 'sounds/sprint-done.wav'
11const OVERRIDE = '!!'
12const MAX_HELD = 20
13
14const JUDGE = [
15 'You guard a focus sprint. The person committed to ONE task and asked to be',
16 'kept on it. Decide whether their new message belongs to that task.',
17 'Answer with exactly one word: ON or OFF.',
18 'ON: anything that works on the task, plus short follow-ups, answers,',
19 'approvals, corrections ("yes", "do that", "fix it", "try again") and',
20 'anything ambiguous.',
21 'OFF: only a message that clearly starts a different piece of work.',
22].join('\n')
23
24const countdown = (ms: number): string => {
25 const seconds = Math.max(0, Math.ceil(ms / 1000))
26 const pad = (n: number) => String(n).padStart(2, '0')
27
28 return `${pad(Math.floor(seconds / 60))}:${pad(seconds % 60)}`
29}
30
31const clip = (text: string, length: number): string => {
32 const line = text.replace(/\s+/g, ' ').trim()
33
34 return line.length <= length ? line : `${line.slice(0, Math.max(1, length - 3))}...`
35}
36
37type Ask =
38 | { kind: 'status' }
39 | { kind: 'stop' }
40 | { kind: 'start'; minutes: number; task: string }
41
42const parse = (args: string, defaultMinutes: number): Ask => {
43 const text = args.trim()
44
45 if (text === '') {
46 return { kind: 'status' }
47 }
48
49 if (/^(stop|end|off|cancel)$/i.test(text)) {
50 return { kind: 'stop' }
51 }
52
53 const timed = /^(\d{1,3})\s*(?:m|min|mins|minutes)?\s+(.+)$/is.exec(text)
54 const minutes = timed?.[1] === undefined ? defaultMinutes : Number(timed[1])
55
56 return { kind: 'start', minutes: Math.max(1, minutes), task: (timed?.[2] ?? text).trim() }
57}
58
59// What a person typed, at the terminal, in the desktop app or from their phone.
60// Never `sdk`: a headless `claude -p` run or a script is nobody's morning.
61const TYPED = new Set(['composer', 'bridge'])
62
63let timer: Timer | undefined
64
65const disarm = () => {
66 timer?.cancel()
67 timer = undefined
68}
69
70async function finish($: EngineInterface, isEarly: boolean, gain: number) {
71 const running = await read($, sprint)
72
73 if (running === null) {
74 return
75 }
76
77 disarm()
78 await update($, sprint, () => null)
79 $.ui.status(undefined)
80 const waiting = (await read($, held)).length
81 const tail =
82 waiting === 0 ? '' : ` ${waiting} held prompt${waiting === 1 ? ' is' : 's are'} waiting above the prompt.`
83 $.ui.toast(`${isEarly ? 'Sprint ended early.' : 'Sprint done.'}${tail}`, { timeoutMs: 10000 })
84
85 if (!isEarly && gain > 0) {
86 void $.audio.play({ asset: SOUND }, { gain }).catch(() => {})
87 }
88}
89
90async function tick($: EngineInterface, gain: number) {
91 const running = await read($, sprint)
92
93 if (running === null) {
94 disarm()
95
96 return
97 }
98
99 const time = await $.clock.now()
100
101 if (time >= running.endsAt) {
102 await finish($, false, gain)
103
104 return
105 }
106
107 await update($, now, () => time)
108 $.ui.status(`Focus ${countdown(running.endsAt - time)} | ${clip(running.task, 40)}`)
109}
110
111function arm($: EngineInterface, gain: number) {
112 disarm()
113 timer = $.clock.every(1000, () => void tick($, gain))
114}
115
116export const register: Register = (on, options) => {
117 const defaultMinutes = Math.max(1, Number(options.minutes ?? 50))
118 const model = String(options.model ?? 'haiku')
119 const gain = Math.min(4, Math.max(0, Number(options.volume ?? 1)))
120
121 on('session.start', async ($, e, next) => {
122 await $.command.register({
123 name: 'sprint',
124 description: 'Start a focus sprint on one task. Off-task prompts are held until it ends.',
125 argumentHint: '[minutes] <task> | stop',
126 })
127
128 // A reload or a resumed session picks the running sprint back up.
129 if ((await read($, sprint)) !== null) {
130 arm($, gain)
131 await tick($, gain)
132 }
133
134 return next(e)
135 })
136
137 on('command.run', { command: 'sprint' }, async ($, e) => {
138 const ask = parse(e.args, defaultMinutes)
139 const running = await read($, sprint)
140 const time = await $.clock.now()
141
142 if (ask.kind === 'status') {
143 return {
144 text:
145 running === null
146 ? 'No sprint is running. Start one with /sprint 50 write the sales page'
147 : `Focus sprint: "${running.task}", ${countdown(running.endsAt - time)} left. End it with /sprint stop`,
148 }
149 }
150
151 if (ask.kind === 'stop') {
152 if (running === null) {
153 return { text: 'No sprint is running.' }
154 }
155
156 await finish($, true, gain)
157
158 return { text: `Sprint on "${running.task}" ended.` }
159 }
160
161 const next: Sprint = { task: ask.task, startedAt: time, endsAt: time + ask.minutes * 60_000 }
162 await update($, sprint, () => next)
163 await update($, now, () => time)
164 arm($, gain)
165 $.ui.status(`Focus ${countdown(next.endsAt - time)} | ${clip(next.task, 40)}`)
166
167 return {
168 text: `Focus lock on for ${ask.minutes} minutes: "${ask.task}". Anything off task is held until the timer ends. Start a prompt with ${OVERRIDE} to push it through.`,
169 }
170 })
171
172 on('prompt.submit', async ($, e, next) => {
173 const running = await read($, sprint)
174
175 if (running === null) {
176 return next(e)
177 }
178
179 const time = await $.clock.now()
180
181 if (time >= running.endsAt) {
182 await finish($, false, gain)
183
184 return next(e)
185 }
186
187 // Only what the person types is judged: never a task notification, a
188 // scheduled run or another plugin's prompt.
189 const isTyped = TYPED.has(e.origin.kind)
190 const text = e.text.trim()
191
192 if (!isTyped || text === '' || text.startsWith('/')) {
193 return next(e)
194 }
195
196 if (text.startsWith(OVERRIDE)) {
197 return next({ ...e, text: text.slice(OVERRIDE.length).trimStart() })
198 }
199
200 const verdict = await $.model.complete({
201 model,
202 system: JUDGE,
203 prompt: `The task: ${running.task}\n\nThe new message:\n${text.slice(0, 2000)}`,
204 maxTokens: 8,
205 effort: 'low',
206 timeoutMs: 8000,
207 })
208
209 // No answer means no lock: a judge that is down never blocks the work.
210 const isOff = verdict.isAnswered && /\bOFF\b/i.test(verdict.text) && !/\bON\b/i.test(verdict.text)
211
212 if (!isOff) {
213 return next(e)
214 }
215
216 await update($, held, list => [...list, e.text].slice(-MAX_HELD))
217
218 return {
219 drop: `Focus lock: that is not "${clip(running.task, 60)}". Held for ${countdown(running.endsAt - time)}, until the sprint ends. Start it with ${OVERRIDE} to push it through, or /sprint stop to end the sprint.`,
220 }
221 })
222
223 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
224 const running = await read($, sprint)
225 const waiting = await read($, held)
226
227 if (e.props.hasSurvey || (running === null && waiting.length === 0)) {
228 return next(e)
229 }
230
231 const { Box, Button, Text } = $.ui.resolve(e)
232 const width = Math.max(20, e.props.bodyColumns)
233
234 if (running !== null) {
235 const time = Math.max(await read($, now), running.startedAt)
236
237 return (
238 <Box flexDirection="column">
239 <Box>
240 <Text bold>FOCUS {countdown(running.endsAt - time)} </Text>
241 <Text wrap="truncate-end">{clip(running.task, width - 40)} </Text>
242 <Button key="end" label="End sprint" onPress={() => finish($, true, gain)} />
243 </Box>
244 {waiting.length > 0 && (
245 <Text dimColor>
246 {waiting.length} off-task prompt{waiting.length === 1 ? '' : 's'} held until the timer ends.
247 </Text>
248 )}
249 </Box>
250 )
251 }
252
253 return (
254 <Box flexDirection="column">
255 <Text bold>Held during your sprint:</Text>
256 {waiting.slice(0, 5).map((text, index) => (
257 <Box>
258 <Button
259 key={`send-${index}`}
260 label="Send"
261 onPress={async () => {
262 await update($, held, list => list.filter((_, at) => at !== index))
263 await $.prompt.submit({ text })
264 }}
265 />
266 <Text> </Text>
267 <Button
268 key={`drop-${index}`}
269 label="Drop"
270 onPress={() => update($, held, list => list.filter((_, at) => at !== index))}
271 />
272 <Text wrap="truncate-end"> {clip(text, width - 22)}</Text>
273 </Box>
274 ))}
275 {waiting.length > 5 && <Text dimColor>and {waiting.length - 5} more</Text>}
276 <Box>
277 <Button key="clear" label="Clear all" onPress={() => update($, held, () => [])} />
278 </Box>
279 </Box>
280 )
281 })
282}
283types/index.d.ts 15 lines1export type Sprint = { task: string; startedAt: number; endsAt: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'focus-lock': {
6 /** The running sprint, or null between sprints. */
7 sprint: Sprint | null
8 /** Prompts held back during a sprint, oldest first. */
9 held: string[]
10 /** The clock as last ticked, so the countdown redraws. */
11 now: number
12 }
13 }
14}
15