SLOPSHOPPER

fresh-start

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

newspinnercommandtoasttimer
v0.1.0MITupdated 2026-10-07razzamatazm/fresh-start
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · fresh-start
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /fresh ⎿ fresh-start: Writing handoff to /tmp/claude-handoffs/2025-10-09T08-53-20-020Z-preview-.md; the session clears when it lands ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts auto-accept edits writing handoff...
README

fresh-start

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:

  1. Claude writes a handoff doc to /tmp/claude-handoffs/<timestamp>-<session>.md: goal, current state, next steps, decisions, open questions, suggested skills.
  2. Once the file exists, the session runs /clear.
  3. The prompt box fills with a "continue from this handoff doc" prompt. Press Enter to start.

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.

Install

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.

Develop

claude plugin validate .
claude plugin test .
Source 2 files
hooks/register.tsx 129 lines
1import { 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}
129
types/index.d.ts 8 lines
1export type Phase = 'idle' | 'writing' | 'clearing'
2
3declare module 'claude-code' {
4  interface PluginState {
5    'fresh-start': { phase: Phase }
6  }
7}
8