Companion to the clock mod: drops a clock wake that arrives twice for the same firing, which can happen when Claude Code replaces a lost hooks worker and…

A small companion to the clock mod. It drops a clock wake that arrives twice for the same firing.
When Claude Code replaces a lost hooks worker, a $.prompt.submit that a mod made from a timer can be made again with the same input. The clock submits its wakes from a timer, so on a heavy session the same wake can arrive twice, a few minutes apart, with the same carry it had already read.
A mod's own prompt.submit hook doesn't see the prompts that mod submits, so the clock can't guard itself. This mod does it instead. The wake header line ([clock wake: <id>] Live time <date> <time> ...) already names exactly one firing, so it is the key: if a wake arrives with the same header as the last one let through for that wake, it's dropped. The next firing has a different date or time, so it passes. If the mod's store can't be read, the wake goes through anyway, because a rare double is better than a lost wake.
Load it alongside the clock, for example:
CLOCK_HOME=1 CLAUDE_CODE_PLUGIN_DIRS="$HOME/.claude/mods/clock:$HOME/.claude/mods/clock-guard" claude --continue
claude plugin test .
The test replays the same submit through a stand-in clock and checks that only the copy is dropped. It fails with the guard turned off, so it proves something real.
The guard and its test are by Ray and Starling (Our Lighthouse), who found the double wake in their own room, traced it to the replayed timer submit, and wrote the fix. Folded in with their permission, 14 October 2026.
hooks/register.ts 39 lines1import type { Register } from 'claude-code'
2
3// clock-guard: drops a replayed clock wake.
4//
5// When Claude Code replaces a lost hooks worker, a `$.prompt.submit` made from a timer can be made again with
6// the same input, so the same wake arrives twice, minutes apart. A mod's own `prompt.submit` hook doesn't see
7// the prompts that mod submits, so the clock can't guard itself; this companion mod does it instead.
8//
9// The wake header line ("[clock wake: <id>] Live time <date> <time> ...") already names exactly one firing,
10// so it works as the key: if a wake arrives with the same header as the last one let through for that wake,
11// it's a replay and is dropped. A later firing of the same wake has a different date or time, so it passes.
12//
13// Guard and test by Ray and Starling (Our Lighthouse), folded in with their permission, 14 Oct 2026.
14
15export function clockWakeKey(origin: any, text: unknown): { id: string; head: string } | null {
16 if (origin?.kind !== 'plugin' || origin?.name !== 'clock') return null
17 if (typeof text !== 'string' || !text.startsWith('[clock wake: ')) return null
18 const head = text.split('\n', 1)[0]
19 const close = head.indexOf(']')
20 if (close < 0) return null
21 return { id: head.slice('[clock wake: '.length, close), head }
22}
23
24export const register: Register = on => {
25 on('prompt.submit', async ($, e, next) => {
26 const wake = clockWakeKey((e as any).origin, (e as any).text)
27 if (!wake) return next(e)
28 try {
29 if ((await $.store.get(`clockWake:${wake.id}`)) === wake.head) {
30 return { drop: `the ${wake.id} wake from that minute already arrived, so this copy was dropped` }
31 }
32 await $.store.set(`clockWake:${wake.id}`, wake.head)
33 } catch {
34 // If the store can't be read or written, let the wake through: a rare double is better than a lost wake.
35 }
36 return next(e)
37 })
38}
39