Prints the session's title and latest recap above the resume hint on exit

A Claude Code mod. When you exit a session, it prints what the session was about right above the claude --resume <id> hint:
Avo dependency updates on main and lc · 2026-10-23 15:05
Updated all Avo gems on main and lc and pushed both branches.
Resume this session with:
claude --resume 56221e9f-b607-4bb8-8b17-7382f6c970fd
The first line is the session title and when you closed it, in your local time. The second is the latest recap Claude wrote for the session, if it wrote one. Nothing prints on /clear.
At the prompt of a Claude Code terminal session:
/plugin install exit-summary --marketplace adrianthedev/claude-exit-summary
Answer y to add the marketplace, then pick a scope.
From a shell:
claude plugin install exit-summary --marketplace adrianthedev/claude-exit-summary
When the session ends, the mod reads the last 256 KiB of the session's transcript (~/.claude/projects/<project>/<session id>.jsonl) to find the newest title and recap. Claude Code has already printed the resume hint by then, so the mod moves the cursor up two lines, clears them, prints the summary, and prints the resume hint again. If a future Claude Code version prints something else after the resume hint, the summary may land in the wrong place.
sh, with a fixed script. The script does one of two things, picked by the first line the mod sends on its standard input:tail: runs tail -c 262144 on the transcript path that follows, so the mod can read the end of a transcript that is too large to read whole.tty: prints the title and the local time (from date) on one line, then copies the rest of its input to /dev/tty, because Claude Code has no API for writing to the terminal after the session closes.HOME, to find the transcript.hooks/register.ts 41 lines1import type { Hook, Register } from 'claude-code'
2
3// The one program the mod runs: sh with a fixed script. What to do comes in on stdin.
4// "tail\n<transcript path>": print the transcript's last 256 KiB (the newest title and recap live there)
5// "tty\n<title>\n<text>": clear the 2-line resume hint, then write "<title> · <local time>" and <text> to the terminal
6const helper = ($: Parameters<Hook<'session.end'>>[0], stdin: string) =>
7 $.process.run(
8 ['sh', '-c', 'read -r mode; case "$mode" in tail) read -r path; tail -c 262144 "$path" ;; tty) read -r title; { printf "\\033[2A\\r\\033[J%s\\033[2m%s\\033[0m\\n" "${title:+$title · }" "$(date "+%Y-%m-%d %H:%M")"; cat; } > /dev/tty ;; esac'],
9 { stdin, timeoutMs: 500 },
10 )
11
12// Pull the newest title and recap out of the transcript's tail. Its first line may be cut, so skip what won't parse.
13const summarize = (jsonl: string) => {
14 let title: string | undefined
15 let recap: string | undefined
16 for (const line of jsonl.split('\n')) {
17 let row
18 try { row = JSON.parse(line) } catch { continue }
19 if (row.type === 'ai-title' || row.type === 'custom-title') title = row.customTitle ?? row.aiTitle ?? title
20 if (row.subtype === 'away_summary') recap = String(row.content).replace(/\s*\(disable recaps in \/config\)\s*$/, '')
21 }
22 return { title: title ? `\x1b[1m${title.replace(/\s+/g, ' ')}\x1b[0m` : '', recap }
23}
24
25export const register: Register = (on) => {
26 on('session.end', async ($, e, next) => {
27 if (e.reason === 'clear') return next(e)
28 try {
29 const home = await $.env.get('HOME')
30 const cwd = await $.session.cwd()
31 const file = `${home}/.claude/projects/${cwd.replace(/[^a-zA-Z0-9]/g, '-')}/${e.sessionId}.jsonl`
32 const { title, recap } = summarize((await helper($, `tail\n${file}\n`)).stdout)
33 // Claude Code has already printed the 2-line resume hint, and no API writes to the exit screen.
34 // So the helper goes up 2 rows and clears to the end; then the title and time, the recap and the hint again.
35 // ponytail: assumes the hint is the last 2 rows; if Claude Code changes its exit output, this lands in the wrong place.
36 await helper($, `tty\n${title}\n${recap ? `${recap}\n` : ''}\nResume this session with:\nclaude --resume ${e.resume.id}\n`)
37 } catch {}
38 return next(e)
39 })
40}
41