/journal writes a markdown timeline of the session (your prompts, commands run, files edited) to .claude/journal/ so you can review or share what happened.

One command turns the session into a shareable markdown timeline.
Quietly records your prompts, shell commands run and files edited. /journal writes them to .claude/journal/<date-time>.md, grouped under each prompt with a files-changed summary. Good for handoffs, standups and PR descriptions.
/plugin marketplace add cloudcruncher/claude-mod-garage
/plugin install session-journal@mod-garage
Run /reload-plugins (or restart Claude Code) afterwards. To try it from a local clone without installing: claude --plugin-dir ./dev-mods/session-journal
/journal any time; run it again to refresh the same file.Illustrative output.
/journal
Journal written to .claude/journal/2026-10-14-09-30.md (23 entries).
The file
# Session 2026-10-14 09:30
## 09:30 · fix the login redirect
- ran `npm test`
- edited `src/auth.ts`
- ran `npm test`
## Files changed
- `src/auth.ts` (×1)
.claude/journal/ to .gitignore if you don't want to commit journals.hooks/register.ts 33 lines1import type { Register } from 'claude-code'
2import { add, fromTool, render, type Entry } from './lib'
3
4export const register: Register = on => {
5 let entries: Entry[] = []
6 let startedAt = 0
7
8 on('session.start', async ($, e, next) => {
9 startedAt = await $.clock.now()
10 await $.command.register({ name: 'journal', description: 'Write a markdown timeline of this session to .claude/journal/' })
11 return next(e)
12 })
13
14 on('prompt.submit', async ($, e, next) => {
15 entries = add(entries, { kind: 'prompt', at: await $.clock.now(), text: String(e.text ?? '') })
16 return next(e)
17 }).catch(($, e, next) => next(e))
18
19 on('tool.call', async ($, e, next) => {
20 const entry = fromTool(e.tool, e as unknown as Record<string, unknown>, await $.clock.now())
21 if (entry) entries = add(entries, entry)
22 return next(e)
23 }).catch(($, e, next) => next(e))
24
25 on('command.run', { command: 'journal' }, async $ => {
26 if (entries.length === 0) return { text: 'Nothing to journal yet.' }
27 const stamp = new Date(startedAt || (await $.clock.now())).toISOString().slice(0, 16).replace(/[:T]/g, '-')
28 const path = `.claude/journal/${stamp}.md`
29 await $.fs.write(path, render(entries, `Session ${stamp.replace(/-(\d\d)-(\d\d)$/, ' $1:$2')}`))
30 return { text: `Journal written to ${path} (${entries.length} entries).` }
31 })
32}
33hooks/lib.ts 42 lines1export type Entry =
2 | { kind: 'prompt'; at: number; text: string }
3 | { kind: 'bash'; at: number; text: string }
4 | { kind: 'edit'; at: number; text: string }
5
6const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit'])
7const MAX_ENTRIES = 1000
8
9const clip = (s: string, n: number) => {
10 const t = s.replace(/\s+/g, ' ').trim()
11 return t.length > n ? t.slice(0, n - 1) + '…' : t
12}
13
14/** Turns a tool call into a journal entry, or undefined if it is not worth recording. */
15export function fromTool(tool: string, args: Record<string, unknown>, at: number): Entry | undefined {
16 if (tool === 'Bash' && typeof args.command === 'string') return { kind: 'bash', at, text: clip(args.command, 200) }
17 const path = args.file_path ?? args.notebook_path
18 if (EDIT_TOOLS.has(tool) && typeof path === 'string') return { kind: 'edit', at, text: path }
19 return undefined
20}
21
22export function add(list: Entry[], entry: Entry): Entry[] {
23 return [...list, entry].slice(-MAX_ENTRIES)
24}
25
26const hhmm = (ms: number) => new Date(ms).toISOString().slice(11, 16)
27
28export function render(entries: Entry[], title: string): string {
29 const lines = [`# ${title} (UTC)`]
30 const files = new Map<string, number>()
31 for (const e of entries) {
32 if (e.kind === 'prompt') lines.push('', `## ${hhmm(e.at)} · ${clip(e.text, 120)}`, '')
33 else if (e.kind === 'bash') lines.push(`- ran \`${e.text.replace(/`/g, "'")}\``)
34 else { lines.push(`- edited \`${e.text}\``); files.set(e.text, (files.get(e.text) ?? 0) + 1) }
35 }
36 if (files.size) {
37 lines.push('', '## Files changed', '')
38 for (const [f, n] of files) lines.push(`- \`${f}\`${n > 1 ? ` (×${n})` : ''}`)
39 }
40 return lines.join('\n') + '\n'
41}
42