Your day, written for you. Every turn is logged; /standup turns today's work across every session on the machine into a three-bullet update, or a Markdown file.

Your day, written for you. Every finished turn is logged with the project, the files touched, and the first line of Claude's answer. /standup gathers today's log across every Claude Code session on the machine and asks a small model for a three-to-five-bullet update. /standup 3 covers three days; --md writes it to a file.
/plugin marketplace add MDmubarak786/claude-mods
/plugin install standup@modhub
Try it for one session without installing:
claude --plugin-dir ./mods/standup
| Command | What it does |
|---|---|
/standup | Today's work, across sessions, as bullets written by Haiku. |
/standup 3 | The last three days. Up to 30. |
/standup raw | The log itself, grouped by project, with no model call. |
/standup --md notes/standup.md | Also write the update to a file, relative to the project root. |
The log a turn adds looks like this, and it's what the model is given:
## app (2 turns, 3 files)
- 09:14 Fixed the login redirect and added a test. [src/login.ts, src/login.test.ts]
- 10:02 Renamed the session helpers. [src/session.ts]
Logs older than 14 days are pruned at session start.
From claude plugin validate ./mods/standup:
hooks: session.start, tool.call{tool=Edit|Write|MultiEdit|NotebookEdit}, turn.complete, command.run{command=standup}
calls: $.command.register, $.fs.write, $.model.complete, $.session.id, $.session.root, $.store.delete (via prune), $.store.get (via gather, record), $.store.keys (via gather, prune), $.store.set (via record), $.ui.log
$.model.complete sends the log to Haiku only when you run /standup without raw, on your plan or API key. The call has no conversation history.$.fs.write writes only the file you name with --md.$.store holds the log, one key per session and day, so concurrent sessions don't overwrite each other.tool.call and turn.complete observe and pass everything through unchanged.claude plugin validate --strict and claude plugin test pass with a stubbed model. /standup answered from a live claude -p session. A live Haiku call hasn't been exercised by the author in a session yet.MIT, see the repository root.
hooks/register.ts 154 lines1// standup: your day, written for you.
2//
3// /standup today's work across every session on this machine, as three bullets
4// /standup 3 the last three days
5// /standup raw the log itself, no model call
6// /standup --md notes.md also write the update to a file
7//
8// Every finished turn in the main conversation is logged: project, files
9// touched, and the first line of Claude's answer. /standup asks a small model
10// to turn the log into an update. Logs older than 14 days are pruned.
11
12const KEEP_DAYS = 14
13const MAX_ENTRIES_PER_KEY = 200
14const ANSWER_CHARS = 240
15
16type Entry = { at: number; root: string; files: string[]; answer: string }
17
18let root = ''
19let sessionId = ''
20let turnFiles = new Set<string>()
21
22function dayOf(ms: number): string {
23 const d = new Date(ms)
24 return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
25}
26
27function firstLine(text: string): string {
28 const line = text.split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#') && !l.startsWith('```')) ?? ''
29 return line.length > ANSWER_CHARS ? line.slice(0, ANSWER_CHARS - 1) + '…' : line
30}
31
32function relative(file: string, base: string): string {
33 return base && file.startsWith(base + '/') ? file.slice(base.length + 1) : file
34}
35
36async function prune($) {
37 const cutoff = dayOf(Date.now() - KEEP_DAYS * 86400000)
38 try {
39 for (const key of await $.store.keys()) {
40 const m = /^standup:(\d{4}-\d{2}-\d{2}):/.exec(key)
41 if (m && m[1] < cutoff) await $.store.delete(key)
42 }
43 } catch {
44 // Pruning is best effort.
45 }
46}
47
48async function record($, answer: string) {
49 const now = Date.now()
50 const key = 'standup:' + dayOf(now) + ':' + sessionId
51 const entry: Entry = { at: now, root, files: [...turnFiles].map((f) => relative(f, root)).sort(), answer: firstLine(answer) }
52 turnFiles = new Set()
53 if (!entry.files.length && !entry.answer) return
54 try {
55 const saved = await $.store.get(key)
56 const entries = Array.isArray(saved) ? saved : []
57 entries.push(entry)
58 await $.store.set(key, entries.slice(-MAX_ENTRIES_PER_KEY))
59 } catch (error) {
60 $.ui.log('standup could not log this turn: ' + error)
61 }
62}
63
64async function gather($, days: number): Promise<Entry[]> {
65 const since = Date.now() - days * 86400000
66 const sinceDay = dayOf(since)
67 const out: Entry[] = []
68 for (const key of await $.store.keys()) {
69 const m = /^standup:(\d{4}-\d{2}-\d{2}):/.exec(key)
70 if (!m || m[1] < sinceDay) continue
71 const saved = await $.store.get(key)
72 if (Array.isArray(saved)) for (const e of saved) if (e && typeof e === 'object' && e.at >= since) out.push(e as Entry)
73 }
74 return out.sort((a, b) => a.at - b.at)
75}
76
77function digest(entries: Entry[]): string {
78 const byProject = new Map<string, Entry[]>()
79 for (const e of entries) {
80 const name = e.root.split('/').pop() || e.root
81 if (!byProject.has(name)) byProject.set(name, [])
82 byProject.get(name)!.push(e)
83 }
84 const lines: string[] = []
85 for (const [project, list] of byProject) {
86 const files = new Set<string>()
87 for (const e of list) for (const f of e.files) files.add(f)
88 lines.push('## ' + project + ' (' + list.length + ' turn' + (list.length === 1 ? '' : 's') + ', ' + files.size + ' file' + (files.size === 1 ? '' : 's') + ')')
89 for (const e of list) {
90 const t = new Date(e.at)
91 lines.push('- ' + String(t.getHours()).padStart(2, '0') + ':' + String(t.getMinutes()).padStart(2, '0') + ' ' + (e.answer || '(no summary)') + (e.files.length ? ' [' + e.files.slice(0, 6).join(', ') + (e.files.length > 6 ? ', +' + (e.files.length - 6) : '') + ']' : ''))
92 }
93 }
94 return lines.join('\n')
95}
96
97export function register(on) {
98 on('session.start', async ($, e, next) => {
99 try {
100 root = await $.session.root()
101 sessionId = await $.session.id()
102 } catch {
103 root = root || ''
104 sessionId = sessionId || 'session'
105 }
106 try {
107 await $.command.register({ name: 'standup', description: 'Summarize what you and Claude did today, across sessions', argumentHint: '[days] [raw] [--md <file>]' })
108 } catch (error) {
109 $.ui.log('could not register /standup: ' + error)
110 }
111 await prune($)
112 return next(e)
113 })
114
115 on('tool.call', { tool: ['Edit', 'Write', 'MultiEdit', 'NotebookEdit'] }, async ($, e, next) => {
116 const result = await next(e)
117 const file = e.tool === 'NotebookEdit' ? e.notebook_path : e.file_path
118 if (typeof e.agentId !== 'string' && typeof file === 'string' && !result.deny && !result.isError) turnFiles.add(file)
119 return result
120 }).catch(async ($, e, next) => next(e))
121
122 on('turn.complete', async ($, e, next) => {
123 if (typeof e.agentId !== 'string' && !e.isAborted) await record($, typeof e.answer === 'string' ? e.answer : '')
124 return next(e)
125 }).catch(async ($, e, next) => next(e))
126
127 on('command.run', { command: 'standup' }, async ($, e) => {
128 const args = e.args.trim().split(/\s+/).filter(Boolean)
129 const md = args.indexOf('--md') >= 0 ? args[args.indexOf('--md') + 1] : null
130 const raw = args.includes('raw')
131 const days = Math.max(1, Math.min(30, Number(args.find((a) => /^\d+$/.test(a)) ?? 1)))
132 const entries = await gather($, days)
133 if (!entries.length) return { text: 'Nothing logged in the last ' + days + ' day(s).' }
134 const log = digest(entries)
135 let text = log
136 if (!raw) {
137 const r = await $.model.complete({
138 model: 'haiku',
139 system: 'You write a short standup update from a log of a developer\'s coding sessions with an AI assistant. Three to five bullets, past tense, first person, concrete: name the projects and what changed. No preamble, no headings.',
140 prompt: log,
141 maxTokens: 400,
142 timeoutMs: 20000,
143 })
144 text = r.isAnswered ? r.text.trim() : 'The model did not answer (' + (r.reason ?? 'unknown') + '), so here is the log:\n' + log
145 }
146 if (md) {
147 const path = md.startsWith('/') ? md : root + '/' + md
148 await $.fs.write(path, '# Standup ' + dayOf(Date.now()) + '\n\n' + text + '\n')
149 text += '\n\nWritten to ' + path
150 }
151 return { text }
152 }).catch(async () => ({ text: 'standup: the command failed.' }))
153}
154