/handoff writes a handoff document for the next session: goal, state, decisions, open items, next steps.

My Claude Code setup: mods, plugins, skills and settings, so a new machine is one clone away.
git clone git@github-personal:aidan-nunn-sys/claude-config.git ~/Work/personal/claude-config
~/Work/personal/claude-config/install.sh
Then:
<SET-ME> values install.sh reports in ~/.claude.json (MCP secrets are never committed).~/.config/mise/config.toml: "npm:optimaizr" = { version = "0.11.0", allow_low_downloads = true }Mods and skills are symlinked from this repo, so edits land here directly. Settings changed through Claude Code live in ~/.claude; run ./capture.sh and review git diff to bring them back.
| Path | What |
|---|---|
mods/ | Mods loaded through CLAUDE_CODE_PLUGIN_DIRS (built by install.sh from this folder) |
plugins/plugins.json | Marketplaces and which marketplace plugins are enabled |
skills/ | Personal skills, symlinked into ~/.claude/skills |
settings/settings.json | ~/.claude/settings.json minus plugin dirs, plugins and hooks; ${HOME} for paths |
settings/mcp.json | User MCP servers, secrets blanked |
settings/rulebook.json | Generic example rules (copied only when the machine has none; not captured) |
docs/specs/ | Design specs |
| Mod | Source | Local changes |
|---|---|---|
| hud | own | prompt hint, context weather bar, cache countdown, kaomoji mascot |
| rulebook | own | hard rules in ~/.claude/rulebook.json, /rules, mcp__rulebook__add_rule |
| xagent | own | dispatch/route tools for Codex, Copilot, Antigravity |
| handoff | own | /handoff [next] writes a handoff doc to ~/.claude/handoffs/ |
| agent-radar | hamzafer/claude-code-mods @ 87f108479bbf | /radar runs mid-turn (immediate) and toggles closed; shows xagent's external agents |
| mission-control | hamzafer/claude-code-mods @ 87f108479bbf | shows xagent's external agents; code-map view (Haiku diff summaries, headless Chrome) stripped |
| blast-radius | hamzafer/claude-code-mods @ 87f108479bbf | 60 s auto-cancel |
| review-watch | hamzafer/claude-code-mods @ 87f108479bbf | none |
| where-am-i | hamzafer/claude-code-mods @ 87f108479bbf | shows only "next" |
| secret-box | ray-amjad/claude-code-mods @ 467b20300d8e | none |
| task-board | MarcoCarnevali/claude-code-mods @ a3e528dd00ce | /board (not /tasks), no auto-open, mid-turn + toggle |
Upstream commits are the repos' HEAD on 2026-10-14, when this repo was made; installs were from around then. Read upstream changes before pulling them in.
~/.claude/.credentials.json and MCP secrets.diagnose-crash, omarchy, symlinks into /usr/share/omarchy), themes/omarchy.json, the herdr SessionStart hook..claude-plugin/types/ in each mod: SDK types Claude Code writes itself.hooks/register.ts 86 lines1// Handoff: /handoff [what comes next] writes a handoff document for a fresh session.
2// The file goes to ~/.claude/handoffs, never into the repo, so it cannot be committed by accident.
3import type { EngineInterface, Register } from 'claude-code'
4
5const MODEL = 'sonnet'
6const MAX_MESSAGES = 80
7const MAX_TEXT = 1500
8
9export const register: Register = on => {
10 on('session.start', async ($, e, next) => {
11 const r = await next(e)
12 await $.command.register({
13 name: 'handoff',
14 description: 'Write a handoff document so a fresh session can pick this work up',
15 argumentHint: '[what the next session should do]',
16 }).catch(() => {}) // a name Claude Code already has is refused: start anyway
17 return r
18 })
19
20 on('command.run', { command: 'handoff' }, async ($, e) => {
21 const cwd = await $.session.cwd()
22 const messages = (await $.session.messages()).slice(-MAX_MESSAGES)
23 if (messages.length === 0) return { text: 'Nothing to hand off yet: the session has no messages.' }
24
25 const [status, log] = await Promise.all([
26 git($, cwd, ['status', '--short', '--branch']),
27 git($, cwd, ['log', '-8', '--oneline']),
28 ])
29 const r = await $.model.complete({
30 model: MODEL,
31 maxTokens: 2500,
32 system: SYSTEM,
33 prompt: [
34 `Working directory: ${cwd}`,
35 `<git-status>\n${status}\n</git-status>`,
36 `<git-log>\n${log}\n</git-log>`,
37 `<transcript>\n${transcript(messages)}\n</transcript>`,
38 e.args.trim() !== '' ? `The person says the next session should: ${e.args.trim()}` : '',
39 'Write the handoff document for the transcript above now: the markdown only, not a reply to it.',
40 ].filter(Boolean).join('\n\n'),
41 })
42 if (!r.isAnswered) return { text: `Could not write a handoff (${r.reason}). Try again.` }
43
44 const home = (await $.env.get('HOME')) ?? ''
45 const path = `${home}/.claude/handoffs/${slug(cwd)}-${stamp()}.md`
46 await $.fs.write(path, `${r.text.trim()}\n`)
47 return {
48 text: [
49 `Handoff written: ${path}`,
50 `Start the next session with: Read ${path} and continue from it.`,
51 ].join('\n'),
52 }
53 })
54}
55
56const SYSTEM =
57 'You write a handoff document so a fresh coding session, with no memory of this one, can continue the work. ' +
58 'Plain words, no em dashes. Markdown with these sections, leaving out one that is empty: ' +
59 '"Goal", "State" (done, in progress, branch and uncommitted files), "Decisions" (each with its reason), ' +
60 '"Open items" (blocked, waiting on the person, unverified), "Next steps" (ordered, concrete, with file paths), ' +
61 '"Gotchas" (things that went wrong or must not be repeated). ' +
62 'Use only what the transcript and git state show: never invent a file, command or result. ' +
63 'Mention no AI model or assistant by name.'
64
65function transcript(messages: { role: string; text: string }[]): string {
66 return messages
67 .filter(m => m.text.trim() !== '')
68 .map(m => `[${m.role === 'user' ? 'person' : 'assistant'}] ${m.text.slice(0, MAX_TEXT)}`)
69 .join('\n')
70}
71
72async function git($: EngineInterface, cwd: string, args: string[]): Promise<string> {
73 const r = await $.process.run(['git', ...args], { cwd }).catch(() => undefined)
74 return r !== undefined && r.exitCode === 0 ? r.stdout.trim() || '(empty)' : '(not a git repository)'
75}
76
77// The directory's last two parts, safe in a file name.
78export function slug(cwd: string): string {
79 const s = cwd.split('/').filter(Boolean).slice(-2).join('-').replace(/[^A-Za-z0-9._-]/g, '-')
80 return s === '' ? 'session' : s
81}
82
83export function stamp(d = new Date()): string {
84 return d.toISOString().slice(0, 16).replace(/[-:]/g, '').replace('T', '-')
85}
86