Ends every turn with the date, time, duration and tool call count

Small Claude Code mods: TypeScript hook modules that run before, after or instead of what Claude Code does.
| Mod | What it does |
|---|---|
turn-timestamp | Ends every turn with ✻ 2026-10-03 19:05 CDT · 3s · 1 tool call, so scrollback in multi-day sessions shows when each turn finished. In the terminal it replaces Claude Code's own end-of-turn line (✻ Churned for 1s); in the desktop Code tab, which draws no such line, it goes under Claude's last reply block. Display only. |
ci-status | /ci opens a pane listing the repo's latest GitHub Actions runs (via gh), refreshes every 30s, shows CI: N running in the status line and a toast when a run finishes. |
ai-meter-agents | Classic command hooks (no TypeScript) that record each session's state (working and on which tool, needs you, done) to ~/.config/claude-usage-monitor/agents/, one file per session. The ai-meter daemon shows them on the desk display's Agents page. The same script feeds Codex, Grok and Antigravity sessions: python3 mods/ai-meter-agents/install_other_tools.py adds it to their hook configs (--uninstall removes it). Silent: never blocks a tool call. |
Installs every mod in setup/mods.conf (mine and the community ones I use) and restores their saved settings, such as the neon-cyber skin:
git clone https://github.com/shissncg/claude-mods ~/Dev/claude-mods
~/Dev/claude-mods/setup.sh
./setup.sh --dev registers this checkout as the marketplace instead of GitHub, so edits under mods/ load without pushing../setup.sh save copies this machine's mod settings into setup/store/; commit them to carry them to other machines.setup/mods.conf. Running the script again only installs what's missing.Plugins turned on at claude.ai sync by themselves and don't need to be listed.
To install just one of mine:
claude plugin marketplace add shissncg/claude-mods
claude plugin install turn-timestamp@shissncg-mods
Or try one for a session without installing:
claude --plugin-dir mods/turn-timestamp --plugin-dir mods/ci-status
Check one with claude plugin validate mods/<name>. Mods need a recent Claude Code build: older CLIs report "session.start" is not an event.
hooks/register.tsx 110 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Stamp } from '../types'
5
6// Each main-loop turn's end: matched to the terminal's end-of-turn row by its
7// duration, and elsewhere to the reply's last text block by its text.
8const stamps = atom({ plugin: 'turn-timestamp', key: 'stamps' } as const, [])
9
10function describe(stamp: Stamp): string {
11 const parts = [stamp.when, took(stamp.durationMs)]
12 if (stamp.tools > 0) parts.push(`${stamp.tools} tool call${stamp.tools === 1 ? '' : 's'}`)
13 return parts.join(' · ')
14}
15
16function took(ms: number): string {
17 const seconds = Math.round(ms / 1000)
18 if (seconds < 60) return `${seconds}s`
19 const minutes = Math.floor(seconds / 60)
20 if (minutes < 60) return `${minutes}m ${seconds % 60}s`
21 return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
22}
23
24export const register: Register = on => {
25 let tools = 0
26 // Durations whose stamp this module drew; one that no row drew (another mod
27 // drew the row, or the turn ended with no reply text) is logged instead.
28 const drawn = new Set<number>()
29
30 on('prompt.submit', ($, e, next) => {
31 tools = 0
32
33 return next(e)
34 })
35
36 on('tool.call', ($, e, next) => {
37 tools += 1
38
39 return next(e)
40 })
41
42 on('turn.complete', async ($, e, next) => {
43 // Subagent turns end inside the main turn; stamp only the main one.
44 if (e.agentId === undefined) {
45 // The host's `date` knows the local time zone; the module's own clock may not.
46 let when: string
47 try {
48 const out = await $.process.run(['date', '+%Y-%m-%d %H:%M %Z'], { timeoutMs: 2000 })
49 when = out.exitCode === 0 ? out.stdout.trim() : ''
50 } catch {
51 when = ''
52 }
53 if (when === '') {
54 when = new Date(await $.clock.now()).toISOString().slice(0, 16).replace('T', ' ') + ' UTC'
55 }
56 const stamp: Stamp = { durationMs: e.durationMs, answer: e.answer.trim(), when, tools }
57 await update($, stamps, list => [...list, stamp].slice(-500))
58
59 const done = await next(e)
60 void (async () => {
61 await $.clock.sleep(1500)
62 if (!drawn.has(stamp.durationMs)) {
63 $.ui.log(`✻ ${describe(stamp)}`)
64 }
65 })()
66 return done
67 }
68
69 return next(e)
70 })
71
72 // The terminal stamps its end-of-turn row (below); the desktop draws no such
73 // row, so there the stamp goes under the reply's last text block.
74 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
75 if (e.surface === 'terminal') {
76 return next(e)
77 }
78 const text = e.props.text.trim()
79 const list = await read($, stamps)
80 const stamp = text === '' ? undefined : list.findLast(one => one.answer !== '' && one.answer.endsWith(text))
81 if (stamp === undefined) {
82 return next(e)
83 }
84
85 drawn.add(stamp.durationMs)
86 const { Box, Text } = $.ui.resolve(e)
87 const reply = await next(e)
88
89 return (
90 <Box flexDirection="column">
91 {reply}
92 <Text dimColor>✻ {describe(stamp)}</Text>
93 </Box>
94 )
95 })
96
97 on('ui.render', { component: 'TurnDuration' }, async ($, e, next) => {
98 const list = await read($, stamps)
99 const stamp = list.findLast(one => one.durationMs === e.props.durationMs)
100 if (stamp === undefined) {
101 return next(e)
102 }
103
104 drawn.add(stamp.durationMs)
105 const { Text } = $.ui.resolve(e)
106
107 return <Text dimColor>✻ {describe(stamp)}</Text>
108 })
109}
110types/index.d.ts 8 lines1export type Stamp = { durationMs: number; answer: string; when: string; tools: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'turn-timestamp': { stamps: Stamp[] }
6 }
7}
8