A band above the prompt showing every message to another agent that is still waiting on a reply.

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 140 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pending } from '../types'
5
6const pending = atom({ plugin: 'agent-comms', key: 'pending' } as const, [] as Pending[])
7
8// MCP tools that hand a question to another agent, from the agentTools option: "tool=Label; tool=Label".
9// The call is the wait.
10export const parseAgentTools = (spec: unknown): Record<string, string> => {
11 const tools: Record<string, string> = {}
12
13 for (const entry of String(spec ?? '').split(';')) {
14 const i = entry.indexOf('=')
15 const tool = (i === -1 ? entry : entry.slice(0, i)).trim()
16
17 if (tool !== '') {
18 tools[tool] = (i === -1 ? '' : entry.slice(i + 1).trim()) || tool
19 }
20 }
21
22 return tools
23}
24
25// A send nobody answers in this long stops being "waiting" and starts being stale.
26const STALE_MS = 30 * 60 * 1000
27
28const PEER_KINDS = new Set(['peer', 'peer-send-message', 'coordinator'])
29
30const clock = (ms: number): string => {
31 const s = Math.max(0, Math.round(ms / 1000))
32
33 return s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
34}
35
36let ticker: { cancel: () => void } | null = null
37
38// The band counts seconds, so a timer redraws it once a second while anything waits.
39function sync(list: Pending[], $: EngineInterface) {
40 if (list.length > 0 && ticker === null) {
41 ticker = $.clock.every(1000, () => $.ui.invalidate('ui.render'))
42 } else if (list.length === 0 && ticker !== null) {
43 ticker.cancel()
44 ticker = null
45 }
46}
47
48export const register: Register = (on, options) => {
49 const AGENT_TOOLS = parseAgentTools(options.agentTools)
50 let seq = 0
51
52 on('tool.call', async ($, e, next) => {
53 let label: string | undefined
54 let kind: Pending['kind'] = 'tool'
55
56 if (e.tool === 'Agent') {
57 kind = 'agent'
58 label = e.name ?? e.description
59 } else if (AGENT_TOOLS[e.tool] !== undefined) {
60 label = AGENT_TOOLS[e.tool]
61 }
62
63 if (label === undefined || e.agentId !== undefined) {
64 return next(e)
65 }
66
67 const id = ++seq
68 const at = await $.clock.now()
69 sync(await update($, pending, list => [...list, { id, kind, label: label as string, at }]), $)
70
71 try {
72 return await next(e)
73 } finally {
74 sync(await update($, pending, list => list.filter(p => p.id !== id)), $)
75 }
76 })
77
78 // SendMessage: delivered means queued, not read. The reply arrives as its own delivery.
79 on('session.send', async ($, e, next) => {
80 const sent = await next(e)
81
82 if (sent.isDelivered && e.agentId === undefined) {
83 const id = ++seq
84 const at = await $.clock.now()
85 sync(await update($, pending, list => [...list, { id, kind: 'send', label: e.to, at }]), $)
86 }
87
88 return sent
89 })
90
91 // A peer delivery answers the oldest open send. The delivery carries no sender, so this is FIFO.
92 on('session.receive', async ($, e, next) => {
93 if (PEER_KINDS.has(e.origin.kind)) {
94 let who: string | undefined
95 sync(
96 await update($, pending, list => {
97 const i = list.findIndex(p => p.kind === 'send')
98
99 if (i === -1) {
100 return list
101 }
102
103 who = list[i].label
104
105 return list.filter((_, j) => j !== i)
106 }),
107 $,
108 )
109 $.ui.toast(who === undefined ? 'Reply came in.' : `Reply from ${who}.`)
110 }
111
112 return next(e)
113 })
114
115 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
116 const list = await read($, pending)
117
118 if (e.props.hasSurvey || list.length === 0) {
119 return next(e)
120 }
121
122 const now = await $.clock.now()
123 const live = list.filter(p => now - p.at < STALE_MS)
124
125 if (live.length === 0) {
126 return next(e)
127 }
128
129 const { Box, Text } = $.ui.resolve(e)
130 const parts = live.map(p => `${p.label} ${clock(now - p.at)}`)
131
132 return (
133 <Box>
134 <Text color="yellow">Waiting on {live.length === 1 ? 'a reply' : `${live.length} replies`}: </Text>
135 <Text dimColor>{parts.join(' | ')}</Text>
136 </Box>
137 )
138 })
139}
140types/index.d.ts 13 lines1export type Pending = {
2 id: number
3 kind: 'send' | 'agent' | 'tool'
4 label: string
5 at: number
6}
7
8declare module 'claude-code' {
9 interface PluginState {
10 'agent-comms': { pending: Pending[] }
11 }
12}
13