/carryover writes a state note from the conversation, clears, and carries the note into the fresh context

Function-hook mods for Claude Code. Each folder is one plugin.
| Mod | What it does | ||
|---|---|---|---|
context-watch | One-line gauge above the prompt for context fill, cost and hot rate limits; toasts at 60/80/90%; compaction keeps your instructions and edited files; /ctx opens a context breakdown pane | ||
live-spinner | Spinner shows what is running: the Bash description or a trimmed command, the ssh host, file being read or edited, search, subagent, MCP tool; a tool's own elapsed time after 15s; "Waiting for signing approval" when a signed git commit or tag sits quiet | ||
pr-watch | Tracks PRs from gh pr / gt submit / git push output, shows checks above the prompt, toasts on green/red/merged, blocks foreground CI polling loops; /prs | ||
house-style | No em dashes: system prompt rule, reminder to the model after a write that adds one, toast when a reply uses one | ||
handoff | /carryover [focus] writes a state note from the conversation and clears; the next prompt carries it. /carryover save, show, drop | ||
agent-jobs | /jobs opens a pane of running subagents and background shells with elapsed time and a Stop button; toasts when background work finishes | ||
zsh-safe | Quotes glob flags zsh would expand (--include=*.ts, find -name *.ts) and keeps a grep that finds nothing from failing a cd ... && grep chain | ||
pm-guard | Blocks npm/npx/yarn where the lockfile says bun or pnpm, and steers to bun where there is no lockfile | ||
remote-guard | Asks before a destructive command (restart, rm, kill, reboot, pct/qm stop, zfs destroy) runs over ssh | ||
xcode-status | Only in a folder with an .xcodeproj/.xcworkspace: one line with the last build or test result (flags runs that executed 0 tests) and the booted simulator; blocks a second xcodebuild while one is running; re-checks on /cd and keeps each project's last result | ||
agent-models | Pins the model each subagent type runs on (Explore, general-purpose, Plan, any other, plus custom type=model pairs), set in /config or with /agent-models <type> <model>; a model the caller names wins unless "Override explicit models" is on | ||
worktrees | /worktrees pane: each git worktree with branch, uncommitted changes, ahead/behind and PR state; Remove (after asking) on clean worktrees whose PR is merged or closed, plus "Remove N merged" to clear every merged one at once | ||
pr-rules | Blocks gh pr create / gt submit when the repo keeps an [Unreleased] changelog and the branch adds no entry (skip with --label no-changelog); where the repo's CLAUDE.md says to label every PR (or requireLabels is on), blocks a PR with no --label and lists the repo's labels. Repos with neither are untouched; `/pr-rules on | off | auto` overrides per repo (kept across sessions) |
claude plugin marketplace add narrowstacks/claude-code-mods
claude plugin install context-watch@claude-code-mods
Or load from a checkout without installing:
claude --plugin-dir ./context-watch --plugin-dir ./live-spinner
claude plugin validate ./context-watch
claude plugin test ./context-watch
tsc -p ./context-watch works once Claude Code has loaded the mod and written .claude-plugin/types/.
hooks/register.ts 123 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Note } from '../types'
5
6// Set once a fresh context starts (a /clear, a new session); the next prompt carries the note.
7const armed = atom({ plugin: 'handoff', key: 'armed' } as const, false)
8
9const MAX_AGE_MS = 24 * 60 * 60 * 1000
10
11const PROMPT = `Write a handoff note so a fresh context can continue this work without the transcript. Plain markdown, under 250 words, these headings only:
12
13## Goal
14## Decisions and constraints
15Every explicit instruction or preference the user gave, verbatim where short.
16## Files touched
17Paths, one line each on what changed.
18## Current state
19What works, what is verified, what is broken or unverified.
20## Next step
21The single next action.
22
23Reply with the note only.`
24
25const noteKey = async ($: EngineInterface) => `note:${await $.session.root()}`
26
27export const readNote = async ($: EngineInterface): Promise<Note | undefined> => {
28 const note = (await $.store.get(await noteKey($))) as Note | undefined
29 if (note === undefined || (await $.clock.now()) - note.at > MAX_AGE_MS) {
30 return undefined
31 }
32
33 return note
34}
35
36const save = async ($: EngineInterface, focus: string) => {
37 const prompt = focus === '' ? PROMPT : `${PROMPT}\n\nThe user wants the note to focus on: ${focus}`
38 const reply = await $.model.fork({ prompt })
39 if (!reply.isAnswered) {
40 return { error: reply.reason === 'nothing-to-fork' ? 'nothing to hand off yet' : `the summary failed (${reply.reason})` }
41 }
42
43 const note: Note = { text: reply.text.trim(), at: await $.clock.now() }
44 await $.store.set(await noteKey($), note)
45
46 return { note }
47}
48
49export const register: Register = on => {
50 on('session.start', async ($, e, next) => {
51 await $.command.register({
52 name: 'carryover',
53 description: 'Save a state note and /clear; the next prompt carries it. Args: [focus] | save | show | drop',
54 })
55 if ((await readNote($)) !== undefined) {
56 await update($, armed, () => true)
57 }
58
59 return next(e)
60 })
61
62 on('command.run', { command: 'carryover' }, async ($, e) => {
63 const args = e.args.trim()
64
65 if (args === 'show') {
66 const note = await readNote($)
67
68 return { text: note?.text ?? 'No handoff note for this project.' }
69 }
70 if (args === 'drop') {
71 await $.store.delete(await noteKey($))
72 await update($, armed, () => false)
73
74 return { text: 'Handoff note dropped.' }
75 }
76
77 const isSaveOnly = args === 'save'
78 const saved = await save($, isSaveOnly ? '' : args)
79 if ('error' in saved) {
80 return { text: `carryover: ${saved.error}.` }
81 }
82 if (isSaveOnly) {
83 return { text: 'Handoff note saved. It rides along with the first prompt after /clear or in a new session here.' }
84 }
85
86 $.command.run({ command: 'clear', args: '' }).then(
87 () => update($, armed, () => true),
88 () => $.ui.toast('Handoff saved. Run /clear to start fresh with it.'),
89 )
90
91 return { text: 'Handoff note saved. Clearing; your next prompt carries it.' }
92 })
93
94 on('command.run', { command: 'clear' }, async ($, e, next) => {
95 const ran = await next(e)
96 if ((await readNote($)) !== undefined) {
97 await update($, armed, () => true)
98 }
99
100 return ran
101 })
102
103 on('prompt.submit', async ($, e, next) => {
104 if (e.origin.kind !== 'composer' || !(await read($, armed))) {
105 return next(e)
106 }
107
108 const note = await readNote($)
109 await update($, armed, () => false)
110 if (note === undefined) {
111 return next(e)
112 }
113
114 await $.store.delete(await noteKey($))
115 $.ui.toast('Handoff note carried in')
116
117 return next({
118 ...e,
119 context: [...(e.context ?? []), `Handoff note from the previous context (written before /clear):\n\n${note.text}`],
120 })
121 })
122}
123types/index.d.ts 8 lines1export type Note = { text: string; at: number }
2
3declare module 'claude-code' {
4 interface PluginState {
5 handoff: { armed: boolean }
6 }
7}
8