Shows how long Claude has been working on the current reply, and how long the last one took, above the prompt.

A Claude Code mod that shows how long Claude is taking on a reply, right above the prompt.
⏱ Working · 1m 12s while Claude works, updated every second
✓ Done in 9s when the reply finishes
✓ Stopped after 42s when you interrupt it
Only the main conversation is timed, so subagents don't reset the timer. It works in the terminal and in the Claude desktop app.
Tested with Claude Code 2.1.310.
In a Claude Code terminal session:
/plugin install turn-timer --marketplace thecodefeels/claude-code-turn-timer
Answer y to add the marketplace, then pick a scope. Or from a shell:
claude plugin install turn-timer --marketplace thecodefeels/claude-code-turn-timer
In the Claude desktop app, click + next to the prompt box, then Plugins → Add marketplace → thecodefeels/claude-code-turn-timer → Add plugin.
Remote sessions: a session on a remote machine (for example over SSH) loads plugins from that machine. Install the mod there too.
claude plugin uninstall turn-timer
Run it from a local checkout without installing it:
claude --plugin-dir ./claude-code-turn-timer
Validate it:
claude plugin validate ./claude-code-turn-timer
The hooks are in hooks/register.tsx, and the state types are in types/index.d.ts.
hooks/register.tsx 64 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Timing } from '../types'
5
6const idle: Timing = { isRunning: false, startedAt: 0, lastMs: null, wasAborted: false }
7const timing = atom({ plugin: 'turn-timer', key: 'timing' } as const, idle)
8const now = atom({ plugin: 'turn-timer', key: 'now' } as const, 0)
9
10function format(ms: number): string {
11 const total = Math.max(0, Math.round(ms / 1000))
12 if (total >= 3600) return `${Math.floor(total / 3600)}h ${Math.floor((total % 3600) / 60)}m`
13 if (total >= 60) return `${Math.floor(total / 60)}m ${total % 60}s`
14 return `${total}s`
15}
16
17export const register: Register = on => {
18 let tick: { cancel: () => void } | undefined
19
20 on('turn.start', async ($, e, next) => {
21 if (e.agentId === undefined) {
22 const startedAt = await $.clock.now()
23 await update($, timing, t => ({ ...t, isRunning: true, startedAt }))
24 await update($, now, () => startedAt)
25 tick?.cancel()
26 tick = $.clock.every(1000, async () => {
27 const at = await $.clock.now()
28 await update($, now, () => at)
29 })
30 }
31 return next(e)
32 })
33
34 on('turn.complete', async ($, e, next) => {
35 if (e.agentId === undefined) {
36 tick?.cancel()
37 tick = undefined
38 await update($, timing, t => ({ ...t, isRunning: false, lastMs: e.durationMs, wasAborted: e.isAborted }))
39 }
40 return next(e)
41 })
42
43 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
44 const t = await read($, timing)
45 if (e.props.hasSurvey || (!t.isRunning && t.lastMs === null)) return next(e)
46
47 const { Box, Text } = $.ui.resolve(e)
48 if (t.isRunning) {
49 const elapsed = (await read($, now)) - t.startedAt
50 return (
51 <Box>
52 <Text dimColor>⏱ Working · {format(elapsed)}</Text>
53 </Box>
54 )
55 }
56 const label = t.wasAborted ? 'Stopped after' : 'Done in'
57 return (
58 <Box>
59 <Text dimColor>✓ {label} {format(t.lastMs ?? 0)}</Text>
60 </Box>
61 )
62 })
63}
64types/index.d.ts 8 lines1export type Timing = { isRunning: boolean; startedAt: number; lastMs: number | null; wasAborted: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'turn-timer': { timing: Timing; now: number }
6 }
7}
8