One press: write a handoff doc, /clear, and pre-fill a prompt to continue from it.

A Claude Code mod that turns "write a handoff, clear, pick it back up" into one press.
Press [ handoff + clear ] at the right end of the prompt footer, or type /fresh [what the next session is for]. Then:
/tmp/claude-handoffs/<timestamp>-<session>.md: goal, current state, next steps, decisions, open questions, suggested skills./clear.If the turn is interrupted or no file appears, the session is left alone and a toast says so. Nothing runs on its own: only the button and /fresh start a handoff.
At the prompt of a terminal Claude Code session:
/plugin install fresh-start --marketplace razzamatazm/fresh-start
Answer y to add the marketplace, then press Enter to pick the user scope. It loads in that session right away and in every session after.
claude plugin validate .
claude plugin test .hooks/register.tsx 129 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Phase } from '../types'
5
6const phase = atom({ plugin: 'fresh-start', key: 'phase' } as const, 'idle' as Phase)
7
8const HANDOFF_DIR = '/tmp/claude-handoffs'
9
10export function handoffPrompt(path: string, focus: string): string {
11 const focusLine = focus
12 ? `The next session will focus on: ${focus}. Tailor the doc to that.`
13 : 'Assume the next session continues the current work where it stands.'
14
15 return [
16 `Write a handoff document for this conversation to ${path} with the Write tool, so a fresh session with no memory of this one can continue the work. Do nothing else this turn: no other edits, no commits, no questions.`,
17 '',
18 focusLine,
19 '',
20 'Cover, in this order:',
21 '1. Goal: what we are trying to get done and why.',
22 '2. Current state: what is done, what is half done, what is verified and how. Name the branch, worktree path, open PRs and running background processes.',
23 '3. Next steps: the exact next action first, then the rest in order.',
24 '4. Decisions and constraints: calls already made (and by whom) that the next session must not re-litigate, plus gotchas hit along the way.',
25 '5. Open questions: anything waiting on the user.',
26 '6. Suggested skills: which skills the next session should invoke with the Skill tool, and when.',
27 '',
28 'Reference specs, plans, issues, commits and diffs by path or URL instead of copying them. Redact secrets and personal data. After writing, reply with one line: the path.',
29 ].join('\n')
30}
31
32export function continuePrompt(path: string): string {
33 return [
34 `Continue from where we left off. Read the handoff doc at ${path} in full before doing anything else.`,
35 'Treat its decisions as settled, check that the current state it describes still holds (git status, branch, open PRs), invoke the skills it suggests, then pick up with its first next step.',
36 'If anything in the doc no longer matches reality, tell me what changed before acting on it.',
37 ].join(' ')
38}
39
40// The handoff in flight; a reload mid-handoff drops it and the band offers the button again.
41let pendingPath: string | null = null
42
43async function start($: EngineInterface, focus: string): Promise<string> {
44 if ((await read($, phase)) !== 'idle') {
45 return 'A handoff is already in progress.'
46 }
47
48 const id = await $.session.id()
49 const stamp = new Date(await $.clock.now()).toISOString().replace(/[:.]/g, '-')
50 pendingPath = `${HANDOFF_DIR}/${stamp}-${id.slice(0, 8)}.md`
51 await update($, phase, () => 'writing')
52 const text = handoffPrompt(pendingPath, focus.trim())
53 // A submit from inside the command's own hook would wait on that hook; send it after.
54 $.clock.after(0, () => void $.prompt.submit({ text }))
55
56 return `Writing handoff to ${pendingPath}; the session clears when it lands.`
57}
58
59async function reset($: EngineInterface): Promise<void> {
60 pendingPath = null
61 await update($, phase, () => 'idle')
62}
63
64export const register: Register = on => {
65 on('session.start', async ($, e, next) => {
66 await $.command.register({
67 name: 'fresh',
68 description: 'Write a handoff doc, clear the session, and pre-fill a prompt to continue from it',
69 argumentHint: '[what the next session is for]',
70 })
71
72 return next(e)
73 })
74
75 on('command.run', { command: 'fresh' }, async ($, e) => ({ text: await start($, e.args ?? '') }))
76
77 on('turn.complete', async ($, e, next) => {
78 const result = await next(e)
79 const path = pendingPath
80 if (e.agentId !== undefined || path === null || (await read($, phase)) !== 'writing') {
81 return result
82 }
83
84 if (e.isAborted || e.reason !== 'answer' || !(await $.fs.exists(path))) {
85 await reset($)
86 $.ui.toast('Handoff not written; session left as it was.')
87
88 return result
89 }
90
91 await update($, phase, () => 'clearing')
92 // Out of the hook first: a command run inside the hook the turn waits on is refused.
93 $.clock.after(0, () => {
94 void (async () => {
95 try {
96 await $.command.run({ command: 'clear' })
97 await $.prompt.fill({ text: continuePrompt(path) })
98 $.ui.toast('Fresh session ready. Press Enter to continue.')
99 } catch (error) {
100 $.ui.toast(`Clear failed: ${String(error)}. Handoff is at ${path}`)
101 } finally {
102 await reset($)
103 }
104 })()
105 })
106
107 return result
108 })
109
110 // The right end of the footer under the prompt, where the mode labels sit; this draws
111 // the engine's labels as it would and the button after them, so it is always there.
112 on('ui.render', { component: 'SessionMode' }, async ($, e) => {
113 const current = await read($, phase)
114 const { Box, Button, Text } = $.ui.resolve(e)
115 const modes = e.props.modes.join(' & ')
116
117 return (
118 <Box>
119 {modes ? <Text dimColor>{modes} </Text> : null}
120 {current === 'idle' ? (
121 <Button key="fresh" label="handoff + clear" dimColor onPress={() => void start($, '')} />
122 ) : (
123 <Text dimColor>{current === 'writing' ? 'writing handoff...' : 'clearing...'}</Text>
124 )}
125 </Box>
126 )
127 })
128}
129types/index.d.ts 8 lines1export type Phase = 'idle' | 'writing' | 'clearing'
2
3declare module 'claude-code' {
4 interface PluginState {
5 'fresh-start': { phase: Phase }
6 }
7}
8