Optional timestamps on user prompts and tool results, switched by the model or the person.

Claude Code gets a date, but not a time. This mod fixes that, a little cheekily.
When timestamps are on:
[tool result recorded at HH:MM:SS ±HH:MM]<system-reminder> saying The time is HH:MM:SS ±HH:MM.Both come from the same local clock. They are off by default, so a session that doesn't need the time pays nothing for it.
timestamps tool with enabled: true or false. A static section added to the system prompt tells it the tool exists and what the stamps mean./timestamps flips it, /timestamps on and /timestamps off set it.Turning timestamps off leaves a one-time note saying the earlier times are stale, so the model does not keep treating the last one as the current time.
session.append on the tool-result door appends the trailer to each tool_result. Whether a result is stamped is decided when its call started, so a toggle in the same parallel batch doesn't change calls that began earlier.session.append on the prompt door appends a separate meta row ($.session.append) with the reminder, so the TUI doesn't show it as something you typed.prompt.compose adds one static, session-scoped section, so the prompt cache is unaffected.The engine's own Today's date is … reminder is left alone: engine rows can't be refused.
At the prompt of a terminal session:
/plugin install date-time --marketplace ioctlLR/claude-needs-times-too
From a shell:
claude plugin install date-time --marketplace ioctlLR/claude-needs-times-too
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with ioctlLR/claude-needs-times-too, then Add plugin.
session.append and prompt.compose do not run in an installed plugin where an organization manages Claude Code; only a plugin the organization lists runs them.claude plugin test .hooks/register.ts 95 lines1import type { EngineInterface, Register } from 'claude-code'
2import {
3 NOTE, OFF_TEXT, TOOL, formatTime, hasText, offTrailer, parseSwitch, promptReminder, reminder, stampBlock, trailer,
4} from './stamp'
5
6const REF = { plugin: 'date-time', key: 'timestamps' } as const
7
8async function read($: EngineInterface) {
9 return (await $.state.get(REF)).value ?? { isOn: false, isOffPending: false }
10}
11
12// Whether timestamps were on when each tool call started, by tool_use_id: a toggle
13// made mid-batch then changes only calls that start after it.
14const startedOn = new Map<string, boolean>()
15
16export const register: Register = on => {
17 on('session.start', async ($, e, next) => {
18 await $.tool.register({
19 name: TOOL,
20 description:
21 'Turn harness timestamps on or off. When on, each tool result ends with its local time and each user prompt is followed by the current time. Off by default; turn on when the time matters, off when it does not.',
22 inputSchema: {
23 type: 'object',
24 properties: { enabled: { type: 'boolean', description: 'true to add timestamps, false to stop' } },
25 required: ['enabled'],
26 },
27 isDeferred: false,
28 })
29 await $.command.register({ name: TOOL, description: 'Turn timestamps on or off (on, off, or no argument to flip)' })
30 return next(e)
31 })
32
33 on('tool.call', { tool: 'mcp__date-time__timestamps' }, async ($, e) => {
34 const isOn = (e as unknown as { enabled?: unknown }).enabled === true
35 await $.state.set(REF, { isOn, isOffPending: false })
36 startedOn.set(e.tool_use_id, isOn)
37 return { result: isOn ? 'Timestamps are on.' : OFF_TEXT }
38 })
39
40 on('tool.call', async ($, e, next) => {
41 if (e.tool !== 'mcp__date-time__timestamps') startedOn.set(e.tool_use_id, (await read($)).isOn)
42 return next(e)
43 }).catch(($, e, next) => next(e))
44
45 on('command.run', { command: 'timestamps' }, async ($, e) => {
46 const cur = await read($)
47 const isOn = parseSwitch(e.args, cur.isOn)
48 if (isOn === null) return { text: 'Usage: /timestamps [on|off]' }
49 await $.state.set(REF, { isOn, isOffPending: !isOn && cur.isOn })
50 return { text: `Timestamps ${isOn ? 'on' : 'off'}.` }
51 })
52
53 on('session.append', { door: 'tool-result' }, async ($, e, next) => {
54 const s = await read($)
55 let line: string | null = null
56 const lineFor = async (id: unknown) => {
57 const wasOn = typeof id === 'string' ? startedOn.get(id) ?? s.isOn : s.isOn
58 if (typeof id === 'string') startedOn.delete(id)
59 if (wasOn) return (line ??= trailer(formatTime(await $.clock.now())))
60 if (s.isOffPending && !s.isOn) return offTrailer
61 return null
62 }
63 let isNoticeUsed = false
64 const content = []
65 for (const b of e.message.content) {
66 const l = b.type === 'tool_result' ? await lineFor(b.tool_use_id) : null
67 if (l === offTrailer) isNoticeUsed = true
68 content.push(l === null ? b : stampBlock(b, l))
69 }
70 if (isNoticeUsed) await $.state.set(REF, { isOn: false, isOffPending: false })
71 return next({ ...e, message: { ...e.message, content } })
72 }).catch(($, e, next) => next(e))
73
74 on('session.append', { door: 'prompt' }, async ($, e, next) => {
75 const stored = await next(e)
76 if (!hasText(e.message.content)) return stored
77 const s = await read($)
78 let text: string | null = null
79 if (s.isOn) text = promptReminder(formatTime(await $.clock.now()))
80 else if (s.isOffPending) {
81 text = reminder(OFF_TEXT)
82 await $.state.set(REF, { isOn: false, isOffPending: false })
83 }
84 if (text !== null) {
85 await $.session.append({ message: { type: 'user', content: [{ type: 'text', text }] }, agentId: e.agentId })
86 }
87 return stored
88 }).catch(($, e, next) => next(e))
89
90 on('prompt.compose', async ($, e, next) => {
91 const r = await next(e)
92 return { sections: [...r.sections, { id: `${$.plugin.name}:time`, text: NOTE, scope: 'session' }] }
93 }).catch(($, e, next) => next(e))
94}
95hooks/stamp.ts 55 lines1export function formatTime(ms: number): string {
2 const d = new Date(ms)
3 const p = (n: number) => String(n).padStart(2, '0')
4 const off = -d.getTimezoneOffset()
5 const sign = off < 0 ? '-' : '+'
6 const abs = Math.abs(off)
7 return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())} ${sign}${p(Math.floor(abs / 60))}:${p(abs % 60)}`
8}
9
10export const TOOL = 'timestamps'
11
12export const trailer = (time: string) => `[tool result recorded at ${time}]`
13export const offTrailer = '[timestamps disabled: earlier times in this conversation are stale]'
14export const OFF_TEXT = 'Timestamps are now disabled. Times shown earlier in this conversation are stale; do not treat them as the current time.'
15
16export const NOTE =
17 'The harness can add timestamps to the conversation. They are OFF by default. ' +
18 `Call the ${TOOL} tool with enabled=true when the current time matters (logs, deadlines, durations) and enabled=false when it no longer does; the person can switch them too. ` +
19 'While on, each tool result ends with a trailer "[tool result recorded at HH:MM:SS ±HH:MM]" and each user prompt is followed by a <system-reminder> saying "The time is HH:MM:SS ±HH:MM.", both from the same local clock. ' +
20 'They are added by the harness, not by tools or the user; use them as the current time and do not repeat them back unless asked. ' +
21 'When timestamps are switched off a notice says so, and the times shown before it are stale.'
22
23type Block = { type: string; [field: string]: unknown }
24
25const MARKS = ['[tool result recorded at ', '[timestamps disabled']
26
27/** Appends a trailer line to a tool_result block's content, once. */
28export function stampBlock(b: Block, line: string): Block {
29 if (b.type !== 'tool_result') return b
30 const c = b.content
31 const hasMark = (t: unknown) => typeof t === 'string' && MARKS.some(m => t.includes(m))
32 if (c === undefined || c === null || c === '') return { ...b, content: line }
33 if (typeof c === 'string') return hasMark(c) ? b : { ...b, content: `${c}\n${line}` }
34 if (Array.isArray(c)) {
35 if (c.some(x => x && x.type === 'text' && hasMark(x.text))) return b
36 return { ...b, content: [...c, { type: 'text', text: line }] }
37 }
38 return b
39}
40
41export const reminder = (text: string) => `<system-reminder>\n${text}\n</system-reminder>`
42export const promptReminder = (time: string) => reminder(`The time is ${time}.`)
43
44/** True when a user row holds typed text, so it earns a reminder. */
45export const hasText = (content: readonly Block[]): boolean => content.some(b => b.type === 'text')
46
47/** Parses `/timestamps` arguments: on, off, or empty to flip. */
48export function parseSwitch(args: string, current: boolean): boolean | null {
49 const a = args.trim().toLowerCase()
50 if (a === '') return !current
51 if (['on', 'true', 'enable', 'enabled', '1'].includes(a)) return true
52 if (['off', 'false', 'disable', 'disabled', '0'].includes(a)) return false
53 return null
54}
55types/index.d.ts 8 lines1export type Timestamps = { isOn: boolean; isOffPending: boolean }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'date-time': { timestamps: Timestamps }
6 }
7}
8