Shows how long Claude has been waiting for you after a turn, and nudges you after long turns

A small Claude Code mod that shows how long Claude has been waiting for you, in the band above the prompt:
● waiting for you · 2m 14s
Claude is working.Whatever other mods draw in the band stays there, under the clock. Pairs well with claude-code-account-badge.
Needs Claude Code v2.1.287 or later (claude --version).
claude plugin marketplace add TusharKanjariya/claude-code-wait-clock
claude plugin install wait-clock@claude-code-wait-clock
Or try it for one session without installing:
git clone https://github.com/TusharKanjariya/claude-code-wait-clock.git
claude --plugin-dir ./claude-code-wait-clock/plugins/wait-clock
A mod is code that runs with your permissions, inside Claude Code. Check before you install:
claude plugin validate ./plugins/wait-clock
It reports the hooks session.start, prompt.submit, turn.complete and ui.render (on AbovePrompt), and the calls $.clock.*, $.state.* and $.ui.*. No file, network, process or env access. If the prompt.submit hook ever fails, its .catch passes your prompt through, so it can't block typing.
Tests pass with claude plugin test on Windows. Not yet tried on macOS or Linux. Issues welcome.
hooks/register.tsx 67 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4// When Claude last finished a turn and handed control back; null while it works
5const waitingSince = atom({ plugin: 'wait-clock', key: 'waitingSince' } as const, null)
6
7function ago(ms: number): string {
8 const s = Math.max(0, Math.floor(ms / 1000))
9 if (s < 60) return `${s}s`
10 const m = Math.floor(s / 60)
11 return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`
12}
13
14export const register: Register = on => {
15 on('session.start', async ($, e, next) => {
16 // Keep the "waiting" timer moving
17 $.clock.every(5000, () => $.ui.invalidate('ui.render'))
18 return next(e)
19 })
20
21 // You typed something: Claude is no longer waiting on you
22 on('prompt.submit', async ($, e, next) => {
23 await update($, waitingSince, () => null)
24 return next(e)
25 }).catch(($, e, next) => next(e)) // if this hook fails, never block the prompt
26
27 // The main turn ended: start the waiting clock, and nudge if it was a long one
28 on('turn.complete', async ($, e, next) => {
29 if (e.agentId) return next(e) // a subagent's turn, not the main loop
30 const endedAt = await $.clock.now()
31 await update($, waitingSince, () => endedAt)
32 if (e.durationMs > 60_000) {
33 const sent = await $.ui.notify(`Claude finished after ${ago(e.durationMs)}. Your turn.`)
34 if (!sent.isSent) $.ui.toast('Claude finished. Your turn.')
35 }
36 return next(e)
37 })
38
39 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
40 if (e.props.hasSurvey) return next(e)
41 const { Box, Text } = $.ui.resolve(e)
42 const since = await read($, waitingSince)
43 const others = await next(e)
44
45 let state: JSX.Element
46 if (e.props.isWorking) {
47 state = <Text dimColor>Claude is working</Text>
48 } else if (since !== null) {
49 const waited = (await $.clock.now()) - since
50 state = (
51 <Text color={waited > 120_000 ? 'warning' : 'success'} bold>
52 ● waiting for you · {ago(waited)}
53 </Text>
54 )
55 } else {
56 state = <Text dimColor>ready</Text>
57 }
58
59 return (
60 <Box flexDirection="column">
61 {state}
62 {others}
63 </Box>
64 )
65 })
66}
67types/index.d.ts 6 lines1declare module 'claude-code' {
2 interface PluginState {
3 'wait-clock': { waitingSince: number | null }
4 }
5}
6