After a wrap-up reply, shows buttons to /clear and carry the handoff's next step into a fresh session.

A Claude Code mod. When a reply contains your wrap-up / handoff card, a band appears above the prompt:
╭────────────────────────────────────────────────────────────────────────────────────────╮
│ ✅ Wrap-up done. Start a fresh session? Press 1-4 │
│ Next Up: push the repo │
│ ╭────────────────────────╮ ╭─────────────────────────╮ ╭─────────────╮ ╭────────────╮ │
│ │1: Clear & send Next Up │ │2: Clear & draft Next Up │ │3: Clear only│ │4: Stay here│ │
│ ╰────────────────────────╯ ╰─────────────────────────╯ ╰─────────────╯ ╰────────────╯ │
╰────────────────────────────────────────────────────────────────────────────────────────╯
| Key | Does |
|---|---|
| 1 Clear & send (green) | /clear, then sends the handoff + next step as the first message of the fresh session, so Claude starts on it straight away |
| 2 Clear & draft | /clear, then puts the handoff + next step in the prompt box for you to edit and send |
| 3 Clear only | /clear, nothing carried over |
| 4 Stay here (blue) | Hide the band, keep the session |
Type the digit into an empty prompt, or click.
claude plugin marketplace add lisim/wrap-buttons
claude plugin install wrap-buttons@wrap-buttons
Restart Claude Code after installing. The two options below are optional and use their defaults until set; change them with /plugin configure wrap-buttons in Claude Code.
| Option | Default | Meaning |
|---|---|---|
| Wrap-up marker | Session Handoff Card | Text on a heading line (#) of a reply that means the wrap-up is done. The reply from that line on is the handoff carried over. |
| Next step label | Next Up | The handoff line holding the next step, e.g. - Next Up: ... |
The plugin ships /wrap-buttons:wrap-up. Out of the box it ends your session with a handoff card, which brings up the buttons.
To make it yours, run /wrap-buttons:wrap-up setup (or press 1 on the band shown in your first session). Five questions: when to wrap up, which docs to update (in the project or an outside notes vault), what to do with git, extra card fields, and whether the routine is for all projects (~/.claude/wrap-up.md) or just this one (.claude/wrap-up.md). The project file wins when both exist. Both are plain markdown you can edit.
Yours keeps working. The bundled skill tells Claude to skip it when you have your own wrap-up skill or CLAUDE.md rules, so saying "wrap up" runs yours. This relies on Claude choosing correctly: if your skill's description doesn't mention wrapping up, add "wrap up" to it. You can always run either one by its command.
To get the buttons with your own wrap-up, set the two options above to match its card. The marker must be on a heading line (starting with #). On the setup band in your first session, press 2 (Later / I have my own) to dismiss it for good.
claude plugin validate .
claude plugin test .
Provided as-is, no support. Built on the Claude Code mods API (early access, may change between releases); tested on 2.1.292. Mods run with your permissions: read the ~120 lines in hooks/ and the skill in skills/wrap-up/ before installing.
MIT licensed.
hooks/register.tsx 99 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { carryOver, parseHandoff } from './card'
5
6const handoff = atom({ plugin: 'wrap-buttons', key: 'handoff' } as const, null)
7const setupBand = atom({ plugin: 'wrap-buttons', key: 'setupBand' } as const, false)
8
9// A routine in the project or the home folder means setup is done.
10async function hasRoutine($: EngineInterface, cwd: string) {
11 const home = await $.env.get('HOME')
12 for (const path of [`${cwd}/.claude/wrap-up.md`, ...(home ? [`${home}/.claude/wrap-up.md`] : [])])
13 if (await $.fs.exists(path)) return true
14 return false
15}
16
17// Either button answers the question for good.
18async function closeSetup($: EngineInterface, runSetup: boolean) {
19 await update($, setupBand, () => false)
20 await $.store.set('setupPrompted', true)
21 if (runSetup) await $.command.run({ command: 'wrap-buttons:wrap-up', args: 'setup' })
22}
23
24// Clears the band first: the state may outlive the /clear.
25async function reset($: EngineInterface, carry: 'start' | 'prefill' | 'none', nextLabel: string) {
26 const card = await read($, handoff)
27 await update($, handoff, () => null)
28 await $.command.run({ command: 'clear' })
29 if (!card || carry === 'none') return
30 const text = carryOver(card, nextLabel)
31 if (carry === 'start') await $.prompt.submit({ text, asUser: true })
32 else await $.prompt.fill({ text })
33}
34
35export const register: Register = (on, options) => {
36 const marker = String(options.marker || 'Session Handoff Card')
37 const nextLabel = String(options.nextLabel || 'Next Up')
38
39 on('session.start', async ($, e, next) => {
40 const result = await next(e)
41 if (e.isInteractive && !(await $.store.get('setupPrompted')) && !(await hasRoutine($, e.cwd)))
42 await update($, setupBand, () => true)
43 return result
44 })
45
46 on('turn.complete', async ($, e, next) => {
47 const found = e.reason === 'answer' ? parseHandoff(e.answer, marker, nextLabel) : null
48 if (found) await update($, handoff, () => found)
49 return next(e)
50 })
51
52 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
53 const card = await read($, handoff)
54 if (e.props.hasSurvey || e.props.isWorking) return next(e)
55 const { Box, Button, Text } = $.ui.resolve(e)
56 if (card === null) {
57 if (!(await read($, setupBand))) return next(e)
58 return (
59 <Box flexDirection="column" borderStyle="round" paddingX={1}>
60 <Text bold>👋 Set up your wrap-up routine? Press 1-2</Text>
61 <Box gap={1}>
62 <Box key="b-setup-now" borderStyle="round" borderColor="green" paddingX={1}>
63 <Button key="setup-now" hotkey="1" plain label="Set up now" onPress={() => closeSetup($, true)} />
64 </Box>
65 <Box key="b-setup-later" borderStyle="round" borderColor="blue" paddingX={1}>
66 <Button key="setup-later" hotkey="2" plain label="Later / I have my own" onPress={() => closeSetup($, false)} />
67 </Box>
68 </Box>
69 </Box>
70 )
71 }
72 // Border colour per button: green the suggested one, blue Stay, the rest the default.
73 return (
74 <Box flexDirection="column" borderStyle="round" paddingX={1}>
75 <Text bold>✅ Wrap-up done. Start a fresh session? Press 1-4</Text>
76 <Box key="preview" flexDirection="column" borderStyle="round" borderColor="green" paddingX={1}>
77 <Text dimColor>1 sends, 2 drafts, in a fresh session:</Text>
78 <Text bold color="green">{`${nextLabel}: ${card.next || '(none found, the handoff alone)'}`}</Text>
79 <Text dimColor>{`+ the handoff card (${card.card.split('\n').length} lines)`}</Text>
80 </Box>
81 <Box gap={1}>
82 <Box key="b-start" borderStyle="round" borderColor="green" paddingX={1}>
83 <Button key="start" hotkey="1" plain label={`Clear & send ${nextLabel}`} onPress={() => reset($, 'start', nextLabel)} />
84 </Box>
85 <Box key="b-prefill" borderStyle="round" paddingX={1}>
86 <Button key="prefill" hotkey="2" plain label={`Clear & draft ${nextLabel}`} onPress={() => reset($, 'prefill', nextLabel)} />
87 </Box>
88 <Box key="b-blank" borderStyle="round" paddingX={1}>
89 <Button key="blank" hotkey="3" plain label="Clear only" onPress={() => reset($, 'none', nextLabel)} />
90 </Box>
91 <Box key="b-stay" borderStyle="round" borderColor="blue" paddingX={1}>
92 <Button key="stay" hotkey="4" plain label="Stay here" onPress={() => update($, handoff, () => null)} />
93 </Box>
94 </Box>
95 </Box>
96 )
97 })
98}
99hooks/card.ts 18 lines1import type { Handoff } from '../types'
2
3// The handoff from the marker's heading on, and its next-step line; null when no heading carries the marker.
4export function parseHandoff(answer: string, marker: string, nextLabel: string): Handoff | null {
5 const lines = answer.split('\n')
6 const at = lines.findIndex(l => l.trimStart().startsWith('#') && l.includes(marker))
7 if (at < 0) return null
8 const card = lines.slice(at).join('\n').trim()
9 const line = card.split('\n').find(l => l.includes(nextLabel)) ?? ''
10 const after = line.slice(line.indexOf(nextLabel) + nextLabel.length)
11 const next = after.replace(/^[*_\s]*:?[*_\s]*:?\s*/, '').trim()
12 return { card, next }
13}
14
15export function carryOver(h: Handoff, nextLabel: string): string {
16 return `Continuing from the last session. Its handoff:\n\n${h.card}\n\n${nextLabel}: ${h.next || '(see handoff)'}`
17}
18types/index.d.ts 8 lines1export type Handoff = { card: string; next: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'wrap-buttons': { handoff: Handoff | null; setupBand: boolean }
6 }
7}
8