When a turn creates a tag (git tag, gh release create), a Claude sprite runs across the band above the prompt and crosses a checkered finish line, and the…

Claude Code plugins, published from Sanduhr.
Add the marketplace once, then install any plugin in it by name:
claude plugin marketplace add estevanhernandez-stack-ed/sanduhr-mods
claude plugin install <plugin>@sanduhr-mods
<!-- sanduhr:plugins -->
claude plugin install now-playing --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install finish-line --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install agent-comms --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install decision-logger --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install page-pane --marketplace estevanhernandez-stack-ed/sanduhr-mods
claude plugin install desk-setup --marketplace estevanhernandez-stack-ed/sanduhr-mods
Then ask Claude Code to set up your Desk ("set up my Desk on the second screen"). Every change previews on the desktop and waits for your Apply.
<!-- /sanduhr:plugins -->
Some mods are tied to one person's machines: a home server, a media box, a dashboard. Those aren't published here as code, because the code would carry someone else's hosts. What travels instead is the idea and a prompt. Paste a prompt into Claude Code (the plugin-authoring skill ships with it), fill in the parts in angle brackets, and Claude builds the mod for your setup; Sanduhr's New Mod page then keeps it, checks it and turns it on per folder.
A status-line glyph and a toast when something at home goes down or comes back, read from whatever already knows (an Uptime Kuma instance, a Tailscale tailnet, a Home Assistant), polled slowly over SSH or HTTP. /lab lists every monitor and peer on demand.
Build me a Claude Code mod called homelab-pulse with the plugin-authoring skill.
It polls my home lab every 60 seconds and shows a short status-line entry: a
check mark when everything is up, the count of what is down otherwise, and
"lab ?" when the source can't be reached. It toasts once when a monitor drops
and once when it recovers, never on every poll. A /lab command prints every
monitor and peer with its state.
Source of truth: <how to read your monitors, e.g. `ssh <host> …` printing JSON,
an HTTP endpoint, or `tailscale status --json`>. Run it with $.process.run with
a 20 s timeout, or $.http.fetch; parse only the fields you show.
Keep all state in an atom; no files, no network beyond that one source; no
credentials in the mod (use an SSH host alias or a local-network URL).
Add register.test.ts covering: all up, one down, source unreachable, recovery.
Two characters in the status line while a media server answers, nothing otherwise. No alerts, slow polling, no token.
Build me a Claude Code mod called plex-pip with the plugin-authoring skill.
Every <5> minutes it fetches <your server's unauthenticated health URL, e.g.
http://<host>:32400/identity for Plex, or /System/Ping for Jellyfin> with
$.http.fetch. While the response is ok it shows "🎬🎵" in the status line via
$.ui.status; on any error or non-2xx it shows nothing. Read the URL and the
interval from plugin options (plexUrl, pollMinutes) so the mod ships without my
address in it; do nothing when the URL option is empty. No toasts, no commands,
no state beyond the last result. Add register.test.ts for ok, non-2xx, a thrown
fetch and an empty option.
If you build one, publish it to your own marketplace from Sanduhr (New Mod, a kept mod's menu, Publish to Marketplace…).
hooks/register.tsx 79 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { RUN_MS, HOLD_MS, frame, progress, tagOfTurn } from './race'
5import type { Race } from '../types'
6
7const race = atom({ plugin: 'finish-line', key: 'race' } as const, null as Race | null)
8
9/** The timer redrawing a run, one at a time (a reload drops it with the module). */
10let ticking: { cancel: () => void } | null = null
11
12/** A run starts: the band redraws every 80 ms while the sprite moves, then holds the finish. */
13async function start($: any, tag: string, motion: boolean) {
14 const startedAt = await $.clock.now()
15 await update($, race, () => ({ tag, startedAt }))
16 ticking?.cancel()
17 ticking = $.clock.every(motion ? 80 : 1000, async () => {
18 const now = await $.clock.now()
19 if (now - startedAt > RUN_MS + HOLD_MS) {
20 ticking?.cancel()
21 ticking = null
22 await update($, race, () => null)
23 return
24 }
25 if (motion) $.ui.invalidate('ui.render')
26 })
27}
28
29export const register: Register = (on, options) => {
30 const motion = options.motion !== 'off'
31
32 on('session.start', async ($, e, next) => {
33 await $.command.register({
34 name: 'finish-line',
35 description: 'Replays the finish line, for a tag or the word you give.',
36 argumentHint: '[tag]',
37 })
38 return next(e)
39 })
40
41 on('command.run', { command: 'finish-line' }, async ($, e) => {
42 const tag = e.args.trim().slice(0, 40) || 'v1.0.0'
43 await start($, tag, motion)
44 return { text: `Across the line: ${tag}` }
45 })
46
47 // When a turn ends: a Bash command of that turn (since the person's last prompt) made a tag.
48 // Read from the transcript, so nothing here can stop or change a command.
49 on('turn.complete', async ($, e, next) => {
50 const out = await next(e)
51 const tag = tagOfTurn(await $.session.messages())
52 if (tag) await start($, tag, motion)
53 return out
54 })
55
56 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
57 const r = await read($, race)
58 if (r === null || e.props.hasSurvey || e.props.maxRows < 1) return next(e)
59 const now = await $.clock.now()
60 const p = progress(now, r.startedAt, motion)
61 if (p === null) return next(e)
62 const { Box, Text } = $.ui.resolve(e)
63 const lines = frame(r.tag, p, e.props.bodyColumns, now).slice(0, e.props.maxRows)
64 return (
65 <Box flexDirection="column">
66 {lines.map((line, i) => (
67 <Box key={`l${i}`} flexDirection="row">
68 {line.map((run, j) => (
69 <Text key={`r${i}-${j}`} color={run.color} bold={run.bold} dimColor={run.dim} wrap="truncate-end">
70 {run.text}
71 </Text>
72 ))}
73 </Box>
74 ))}
75 </Box>
76 )
77 })
78}
79hooks/race.ts 160 lines1// The finish line's pure parts: which commands made a tag, and what the band draws at a moment.
2// No $ here, so the tests read every frame without a stub.
3
4/** How long the sprite runs to the line, and how long the finish stays after. */
5export const RUN_MS = 2400
6export const HOLD_MS = 3600
7/** Narrower than this, the band folds to one line. */
8export const FOLD_COLUMNS = 40
9
10export const CLAUDE = '#d97757'
11export const GOLD = '#f5c26b'
12export const TRACK = '#6b6b78'
13
14export type Run = { text: string; color?: string; bold?: boolean; dim?: boolean }
15export type Line = Run[]
16
17/** The tag a shell command created, or null: `git tag <name>` (with -a, -s, -m, -f), and
18 * `gh release create <tag>`. Listing, deleting or verifying tags is no new tag. */
19export function tagFrom(command: string): string | null {
20 for (const words of commands(command)) {
21 const git = words.indexOf('git')
22 if (git >= 0 && words[git + 1] === 'tag') {
23 const rest = words.slice(git + 2)
24 if (rest.some(w => /^(-l|--list|-d|--delete|-v|--verify|--contains|--no-contains|--merged|--points-at|-n\d*)$/.test(w))) continue
25 const name = firstName(rest, ['-m', '--message', '-F', '--file', '-u', '--local-user', '--cleanup'])
26 if (name) return name
27 }
28 const gh = words.indexOf('gh')
29 if (gh >= 0 && words[gh + 1] === 'release' && words[gh + 2] === 'create') {
30 const name = firstName(words.slice(gh + 3), ['-t', '--title', '-n', '--notes', '-F', '--notes-file', '--target', '-R', '--repo', '--discussion-category'])
31 if (name) return name
32 }
33 }
34 return null
35}
36
37/** The tag the last turn made: a Bash call after the person's last prompt that didn't fail. */
38export function tagOfTurn(messages: { role: string; text: string; toolUses: { tool: string; input: Record<string, unknown>; isError?: boolean }[] }[]): string | null {
39 let from = 0
40 for (let i = messages.length - 1; i >= 0; i--) {
41 if (messages[i].role === 'user' && messages[i].text.trim().length > 0) { from = i + 1; break }
42 }
43 let tag: string | null = null
44 for (const m of messages.slice(from)) {
45 for (const use of m.toolUses ?? []) {
46 if (use.tool !== 'Bash' || use.isError === true || typeof use.input.command !== 'string') continue
47 tag = tagFrom(use.input.command) ?? tag
48 }
49 }
50 return tag
51}
52
53/** A command line as its commands' words: quotes kept together, split at && || ; and newlines. */
54export function commands(line: string): string[][] {
55 const out: string[][] = [[]]
56 let word = ''
57 let quote = ''
58 let inWord = false
59 const end = () => { if (inWord) out[out.length - 1].push(word); word = ''; inWord = false }
60 for (let i = 0; i < line.length; i++) {
61 const c = line[i]
62 if (quote) {
63 if (c === quote) quote = ''
64 else word += c
65 continue
66 }
67 if (c === '"' || c === "'") { quote = c; inWord = true; continue }
68 const two = line.slice(i, i + 2)
69 if (two === '&&' || two === '||' || c === ';' || c === '\n') {
70 end()
71 out.push([])
72 if (two === '&&' || two === '||') i++
73 continue
74 }
75 if (/\s/.test(c)) { end(); continue }
76 word += c
77 inWord = true
78 }
79 end()
80 return out.filter(ws => ws.length > 0)
81}
82
83/** The first word that isn't a flag or a flag's value. */
84function firstName(words: string[], takesValue: string[]): string | null {
85 for (let i = 0; i < words.length; i++) {
86 const w = words[i]
87 if (takesValue.includes(w)) { i++; continue }
88 if (w.startsWith('-')) continue
89 return w.length > 0 ? w.slice(0, 40) : null
90 }
91 return null
92}
93
94/** Where the run stands at `now`: 0 at the start, 1 at the line, then holding; null when over. */
95export function progress(now: number, startedAt: number, motion: boolean): number | null {
96 const t = now - startedAt
97 if (t < 0 || t > RUN_MS + HOLD_MS) return null
98 if (!motion) return 1
99 const p = Math.min(1, t / RUN_MS)
100 return 1 - (1 - p) * (1 - p) // eases into the line
101}
102
103const SPRITE_TOP = ' ▐▛███▜▌ '
104const SPRITE_MID = '▝▜█████▛▘'
105const LEGS = [' ▘▘ ▝▝ ', ' ▝▝ ▘▘ ']
106const SPRITE_W = 9
107const CONFETTI = ['✦', '·', '✧', '·', '✦']
108
109/** The tag in a gradient from Claude's orange to gold, one color a letter. */
110export function tagRuns(tag: string): Run[] {
111 const letters = Array.from(tag)
112 return letters.map((ch, i) => ({ text: ch, bold: true, color: mix(CLAUDE, GOLD, letters.length < 2 ? 0 : i / (letters.length - 1)) }))
113}
114
115export function mix(a: string, b: string, t: number): string {
116 const ca = [1, 3, 5].map(i => parseInt(a.slice(i, i + 2), 16))
117 const cb = [1, 3, 5].map(i => parseInt(b.slice(i, i + 2), 16))
118 return '#' + ca.map((v, i) => Math.round(v + (cb[i] - v) * t).toString(16).padStart(2, '0')).join('')
119}
120
121/** The band's lines for a run at progress `p` in `columns` cells, `now` choosing the stride. */
122export function frame(tag: string, p: number, columns: number, now: number): Line[] {
123 const finished = p >= 1
124 const tagWidth = Array.from(tag).length
125 // The finish row: the flag (2), then two spaces, the tag, two spaces and up to 9 of confetti.
126 const line = Math.min(60, columns - 2 - (tagWidth + 13))
127 if (columns < FOLD_COLUMNS || line < SPRITE_W + 4) {
128 const track = Math.max(4, columns - Array.from(tag).length - 8)
129 const x = Math.round(p * track)
130 return [[
131 { text: '·'.repeat(x), color: TRACK, dim: true },
132 { text: '▐█▌', color: CLAUDE, bold: true },
133 { text: '·'.repeat(Math.max(0, track - x)), color: TRACK, dim: true },
134 { text: ' ▚ ', bold: true },
135 ...(finished ? tagRuns(tag) : [{ text: tag, dim: true }]),
136 ]]
137 }
138 const x = Math.round(p * (line - SPRITE_W + 2))
139 const stride = Math.floor(now / 160) % 2
140 const label: Run[] = finished
141 ? [{ text: ' ' }, ...tagRuns(tag), { text: ' ' + CONFETTI.slice(0, 3 + stride).join(' '), color: GOLD }]
142 : [{ text: ' ' + tag, dim: true }]
143 const row = (sprite: string, ground: boolean, flag: string, tail: Run[]): Line => {
144 const before = x
145 const after = Math.max(0, line - x - SPRITE_W)
146 const runs: Line = [
147 { text: ground ? '·'.repeat(before) : ' '.repeat(before), color: TRACK, dim: true },
148 { text: sprite, color: CLAUDE, bold: true },
149 { text: ground ? '·'.repeat(after) : ' '.repeat(after), color: TRACK, dim: true },
150 { text: flag, bold: true },
151 ]
152 return runs.concat(tail)
153 }
154 return [
155 row(SPRITE_TOP, false, '▚▞', []),
156 row(SPRITE_MID, false, '▞▚', label),
157 row(finished ? LEGS[0] : LEGS[stride], true, '▚▞', []),
158 ]
159}
160types/index.d.ts 8 lines1export type Race = { tag: string; startedAt: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'finish-line': { race: Race | null }
6 }
7}
8