Pins standing notes with /pin that Claude keeps following after /compact and /clear; shows them in one row above the prompt or as a counter, and keeps them per…

Pins standing notes with /pin. Claude keeps following them after /compact and /clear, and you see them above the prompt, or as a counter under it. Pins you keep come back after a restart.
<img src="../docs/pin-board.png" alt="Two pins in one row above the prompt" width="542">
/pin <note>: pins a note./pin <note> --keep: pins a note and keeps it for this folder./pin: lists the pins./unpin <n> or /unpin all: removes pins. The rest are renumbered.A pin lasts until you quit Claude Code. A kept pin is stored for the folder you started Claude Code in, and comes back the next time you start it there; a toast then says how many came back, so a forgotten one does not steer Claude unseen.
All pins share one row above the prompt, numbered for /unpin: 📌 1 use pnpm · 2 no force push. A long row is cut at the edge; /pin lists them in full.
If that band is already busy, set Where pins show to counter in /config. Then only the number shows, at the right end of the line under the prompt: 📌 2 pins.
<img src="../docs/pin-board-counter.png" alt="The pin counter at the right end of the line under the prompt" width="698">
/compact and after /clear, so the pins are there each time without being sent with every message.claude plugin marketplace add arasovic/claude-code-mods
claude plugin install pin-board@claude-code-mods
Restart Claude Code.
claude plugin validate .
claude plugin test .
../typecheck.sh pin-boardhooks/register.tsx 100 lines1import type { CommandRunResult, EngineInterface, Register } from 'claude-code'
2
3export type Pin = { text: string; keep?: true }
4
5export function parsePin(args: string): Pin | undefined {
6 const words = args.split(/\s+/).filter(Boolean)
7 const text = words.filter(w => w !== '--keep').join(' ')
8 if (!text) return undefined
9 return words.includes('--keep') ? { text, keep: true } : { text }
10}
11
12export const listText = (pins: readonly Pin[]) => pins.map((p, i) => `${i + 1}. ${p.text}${p.keep ? ' (kept)' : ''}`).join('\n')
13
14export const bandText = (pins: readonly Pin[]) => `📌 ${pins.map((p, i) => `${i + 1} ${p.text}`).join(' · ')}`
15
16export const counterText = (n: number) => `📌 ${n} ${n === 1 ? 'pin' : 'pins'}`
17
18// Later /pin and /unpin notes in the conversation can be newer than this list: the system prompt is only rendered again at start, /compact and /clear.
19export const section = (pins: readonly Pin[]) =>
20 `The user pinned these standing notes. Follow them until the user unpins them. If a later /pin or /unpin message in the conversation disagrees with this list, the later message wins.\n${listText(pins)}`
21
22// Pins live as long as the process, so they outlast /clear; --keep ones are also stored for the folder and come back on the next start.
23let cwd = ''
24let pins: Pin[] = []
25
26async function save($: EngineInterface, next: Pin[]) {
27 pins = next
28 await $.store.set(cwd, pins.filter(p => p.keep))
29 $.ui.invalidate('ui.render')
30}
31
32async function pin($: EngineInterface, args: string): Promise<CommandRunResult> {
33 if (!args.trim()) return { text: pins.length ? listText(pins) : 'No pins. Add one with /pin <note> [--keep].' }
34 const added = parsePin(args)
35 if (!added) return { text: 'Usage: /pin <note> [--keep]' }
36 await save($, [...pins, added])
37 const n = pins.length
38 return {
39 text: `Pinned ${n}: ${added.text}${added.keep ? ' (kept)' : ''}`,
40 context: [`The user pinned standing note ${n}: ${added.text}\nFollow it until the user unpins it.`],
41 }
42}
43
44async function unpin($: EngineInterface, args: string): Promise<CommandRunResult> {
45 const arg = args.trim()
46 if (arg === 'all') {
47 if (!pins.length) return { text: 'No pins.' }
48 await save($, [])
49 return { text: 'Unpinned all.', context: ['The user removed all pinned notes. Stop following them.'] }
50 }
51 const n = Number(arg)
52 const gone = pins[n - 1]
53 if (!Number.isInteger(n) || !gone) return { text: pins.length ? `Usage: /unpin <1-${pins.length}|all>\n${listText(pins)}` : 'No pins.' }
54 await save($, pins.filter((_, i) => i !== n - 1))
55 return {
56 text: `Unpinned ${n}: ${gone.text}`,
57 context: [`The user removed pinned note ${n}: ${gone.text}\nStop following it. The remaining pins are renumbered:\n${listText(pins) || '(none)'}`],
58 }
59}
60
61export const register: Register = (on, options) => {
62 const display = options.display === 'counter' ? 'counter' : 'band'
63
64 on('session.start', async ($, e, next) => {
65 cwd = e.cwd
66 const stored = await $.store.get(cwd)
67 pins = Array.isArray(stored) ? (stored as Pin[]) : []
68 if (pins.length) $.ui.toast(`📌 ${pins.length} kept ${pins.length === 1 ? 'pin' : 'pins'} · /pin to list`, { timeoutMs: 6000 })
69 await $.command.register({ name: 'pin', description: 'Pin a standing note the model keeps following; no note lists the pins', argumentHint: '[note] [--keep]' })
70 await $.command.register({ name: 'unpin', description: 'Remove a pinned note', argumentHint: '<n|all>' })
71 return next(e)
72 })
73
74 on('command.run', { command: 'pin' }, ($, e) => pin($, e.args))
75 on('command.run', { command: 'unpin' }, ($, e) => unpin($, e.args))
76
77 on('prompt.compose', async ($, e, next) => {
78 const result = await next(e)
79 if (!pins.length) return result
80 return { sections: [...result.sections, { id: 'pin-board:pins', text: section(pins), scope: 'session' }] }
81 })
82
83 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
84 // The band is shared with other plugins: keep what they drew beneath, and take one row however many pins there are.
85 const below = await next(e)
86 if (display !== 'band' || e.props.hasSurvey || !pins.length) return below
87 const { Box, Text } = $.ui.resolve(e)
88 return (
89 <Box flexDirection="column">
90 {below}
91 <Text wrap="truncate-end">{bandText(pins)}</Text>
92 </Box>
93 )
94 })
95
96 on('ui.render', { component: 'SessionMode' }, ($, e, next) =>
97 display === 'counter' && pins.length ? next({ ...e, props: { ...e.props, modes: [...e.props.modes, counterText(pins.length)] } }) : next(e),
98 )
99}
100