/you-should-know: ask for a You should know summary on demand, shown above the prompt, instead of waiting for the built-in mod's automatic check

Ask for a "You should know" summary whenever you want one.
Claude Code's built-in You should know mod (cc-plugin-you-should-know@builtin) checks on its own, at every 6th step of a long turn, and you can't start it yourself. This mod adds a command that runs the same kind of check right away. A side copy of the model reads the conversation and lists up to 5 things you might miss: results that look like success but aren't, risky actions, unstated assumptions, changes outside the ask, costs and loose ends.
| Command | What it does |
|---|---|
/you-should-know | Check now |
/you-should-know <focus> | Check with a focus, e.g. /you-should-know tests |
/you-should-know clear | Hide the last summary |
While a check runs, the band shows a spinner and the seconds so far (⠹ You should know · checking… 4s). The command also works mid-turn. If another plugin already took the name, it registers as /ysk.
Each point is one numbered line, most urgent first: a colored tag, a headline of a few words, and an optional dim detail after a dash.
You should know · tests · 3 points
1 Risk Only test is skipped — "OK" is not a pass
2 Verify Edge cases untested — run with the skip removed
3 Changed pricing.py rewritten — clamp added
| Tag | Meaning |
|---|---|
| Risk | Could break, lose data or mislead |
| Verify | Looks done but isn't confirmed |
| To do | Left unfinished |
| Changed | Touched outside the ask |
| Assumed | Decided without saying so |
| Cost | Time, money or tokens |
The summary shows above the prompt with two buttons:
It's also added to the transcript as a dim line. Neither one is sent to the model.
Each check is one model call on your usage. It reuses the main chat's prompt cache, so it costs little while the cache is warm. It doesn't change the main chat's prompt, model or cache.
hooks/register.tsx 308 lines1// You Should Know (local): /you-should-know asks for a "You should know" summary on demand.
2//
3// The built-in You should know mod (cc-plugin-you-should-know) checks on its own
4// every 6th step of a long turn and can't be called by hand. /you-should-know runs the same
5// kind of check right away: a side copy of the model reads this conversation and
6// names what the person might miss. The answer shows above the prompt and as a
7// dim transcript line; neither is sent to the model. "Ask Claude" puts it in the
8// prompt box as a draft, so the person decides whether the main chat sees it.
9//
10// Each point is a tag (Risk, Verify, To do, Changed, Assumed, Cost), a headline of
11// a few words and an optional one-line detail, numbered and sorted most urgent
12// first, so the summary can be taken in at a glance.
13//
14// /you-should-know check now
15// /you-should-know <focus> check with a focus, e.g. /you-should-know tests
16// /you-should-know clear hide the last summary
17
18import { atom, read, update } from 'claude-code'
19import type { EngineInterface, Register, ThemeKey } from 'claude-code'
20
21import type { Note, Point, Tag } from '../types'
22
23const noteAtom = atom({ plugin: 'you-should-know', key: 'note' } as const, null)
24const checkingAtom = atom({ plugin: 'you-should-know', key: 'isChecking' } as const, false)
25
26// While a check runs, the band shows a spinner and the seconds so far, redrawn
27// by a timer. Both live in the module: they only matter while it is drawing.
28const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
29const SPIN_MS = 120
30let frame = 0
31let checkStartedAt = 0
32let spinner: { cancel: () => void } | undefined
33
34function startSpinner($: EngineInterface, now: number): void {
35 spinner?.cancel()
36 frame = 0
37 checkStartedAt = now
38 spinner = $.clock.every(SPIN_MS, () => {
39 frame += 1
40 $.ui.invalidate('ui.render')
41 })
42}
43
44export function spinnerAt(n: number): string {
45 return SPINNER[((n % SPINNER.length) + SPINNER.length) % SPINNER.length]
46}
47
48export function checkingText(seconds: number): string {
49 return ` · checking… ${seconds}s`
50}
51
52function stopSpinner(): void {
53 spinner?.cancel()
54 spinner = undefined
55}
56
57const COMMAND_DESCRIPTION = 'You should know: check this chat now for anything you might miss'
58const COMMAND_HINT = '[focus | clear]'
59const MAX_POINTS = 5
60const NOTHING = 'NOTHING'
61
62// Most urgent first; the summary is sorted in this order. 'Note' is for a point
63// the model gave without one of the others.
64const TAGS: readonly Tag[] = ['Risk', 'Verify', 'To do', 'Changed', 'Assumed', 'Cost', 'Note']
65const TAG_COLOR: Record<Tag, ThemeKey> = {
66 Risk: 'error',
67 Verify: 'warning',
68 'To do': 'suggestion',
69 Changed: 'claude',
70 Assumed: 'permission',
71 Cost: 'remember',
72 Note: 'subtle',
73}
74const TAG_WIDTH = Math.max(...TAGS.map(tag => tag.length))
75// Band columns for the number and the tag. Bold text on desktop runs close to a
76// column per character, so the tag column gets two to spare.
77const NUM_CELLS = 3
78const TAG_CELLS = TAG_WIDTH + 2
79const TAG_MEANING =
80 'Risk = could break, lose data or mislead; Verify = looks done but unconfirmed; ' +
81 'To do = left unfinished; Changed = touched outside the ask; Assumed = decided without saying; ' +
82 'Cost = time, money or tokens'
83
84// The side agent's question. It reads the whole conversation as the main thread
85// sent it, so it only needs the ask itself.
86export function promptFor(focus: string): string {
87 return [
88 'Pause the task. You are a second pair of eyes for the person running this session.',
89 'From this conversation, list what they should know but could easily miss.',
90 focus === '' ? '' : `Focus on: ${focus}.`,
91 `Give at most ${MAX_POINTS} points, most important first, one per line, exactly in this form:`,
92 '- <Tag> | <headline> | <detail>',
93 `Tag is one of: ${TAG_MEANING}.`,
94 'Headline: what it is, 8 words at most. Detail: why it matters or what to do, 14 words at most,',
95 'or leave it empty. Name the file, test or command when there is one.',
96 'Plain words, no preamble, no closing line, and nothing they were already clearly told.',
97 `If there is nothing worth flagging, reply with exactly ${NOTHING}.`,
98 ]
99 .filter(line => line !== '')
100 .join(' ')
101}
102
103const BULLET = /^([-*•]|\d+[.)])\s+/
104
105function tagOf(word: string): Tag | undefined {
106 const key = word.replace(/[[\]*:]/g, '').trim().toLowerCase().replace(/\s+/g, ' ')
107 if (key === 'todo' || key === 'to-do') return 'To do'
108 return TAGS.find(tag => tag.toLowerCase() === key)
109}
110
111// "headline — detail" or "headline: detail" when the model put both in one part.
112function splitLine(text: string): { headline: string; detail: string } {
113 const at = text.search(/\s[—–]\s|:\s/)
114 if (at <= 0) return { headline: text, detail: '' }
115 const sep = text.slice(at).match(/^(\s[—–]\s|:\s)/)?.[0] ?? ''
116 return { headline: text.slice(0, at).trim(), detail: text.slice(at + sep.length).trim() }
117}
118
119function pointOf(line: string): Point {
120 const body = line.replace(BULLET, '').trim()
121 const parts = body.split('|').map(part => part.trim())
122 if (parts.length >= 2) {
123 const tag = tagOf(parts[0])
124 if (tag !== undefined) {
125 const rest = parts.slice(1).filter(part => part !== '')
126 const split = rest.length === 1 ? splitLine(rest[0]) : { headline: rest[0] ?? '', detail: rest.slice(1).join(' — ') }
127 return { tag, ...split }
128 }
129 }
130 // "[Risk] text" or "Risk: text"
131 const lead = body.match(/^\[([^\]]+)\]\s*(.+)$/) ?? body.match(/^([A-Za-z -]{2,8}):\s+(.+)$/)
132 const tag = lead ? tagOf(lead[1]) : undefined
133 if (lead && tag !== undefined) return { tag, ...splitLine(lead[2].trim()) }
134 return { tag: 'Note', ...splitLine(body) }
135}
136
137// The reply's points, most urgent first: its bullet lines, or every non-empty line
138// when it used none.
139export function pointsOf(text: string): Point[] {
140 const trimmed = text.trim()
141 if (trimmed === '' || trimmed.replace(/[.\s]+$/, '').toUpperCase() === NOTHING) return []
142 const lines = trimmed.split('\n').map(line => line.trim()).filter(line => line !== '')
143 const bullets = lines.filter(line => BULLET.test(line))
144 return (bullets.length > 0 ? bullets : lines)
145 .slice(0, MAX_POINTS)
146 .map(pointOf)
147 .filter(point => point.headline !== '')
148 .map((point, i) => ({ point, i }))
149 .sort((a, b) => TAGS.indexOf(a.point.tag) - TAGS.indexOf(b.point.tag) || a.i - b.i)
150 .map(({ point }) => point)
151}
152
153function lineOf(point: Point): string {
154 return point.detail === '' ? point.headline : `${point.headline} — ${point.detail}`
155}
156
157export function summaryOf(note: Note): string {
158 const title = `You should know${note.focus === '' ? '' : ` · ${note.focus}`}`
159 if (note.points.length === 0) return `${title}: nothing worth flagging right now.`
160 return [
161 `${title} (${note.points.length})`,
162 ...note.points.map((point, i) => `${i + 1}. ${point.tag.padEnd(TAG_WIDTH)} ${lineOf(point)}`),
163 ].join('\n')
164}
165
166async function check($: EngineInterface, focus: string): Promise<void> {
167 try {
168 const reply = await $.model.fork({ prompt: promptFor(focus) })
169 if (!reply.isAnswered) {
170 $.ui.toast(
171 reply.reason === 'nothing-to-fork'
172 ? 'Nothing to check yet: send a message first, then /you-should-know.'
173 : `/you-should-know couldn't finish the check (${reply.reason}).`,
174 )
175 return
176 }
177 const note: Note = { points: pointsOf(reply.text), focus, at: await $.clock.now() }
178 await update($, noteAtom, () => note)
179 $.ui.log(summaryOf(note))
180 } catch {
181 $.ui.toast("/you-should-know couldn't finish the check.")
182 } finally {
183 stopSpinner()
184 await update($, checkingAtom, () => false)
185 $.ui.invalidate('ui.render')
186 }
187}
188
189async function askClaude($: EngineInterface): Promise<void> {
190 const note = await read($, noteAtom)
191 if (note === null || note.points.length === 0) return
192 const text = [
193 'About these points from /you-should-know:',
194 ...note.points.map(point => `- ${point.tag}: ${lineOf(point)}`),
195 '',
196 ].join('\n')
197 await $.prompt.fill({ text, mode: 'replace' })
198 await update($, noteAtom, () => null)
199}
200
201// /you-should-know, or /ysk when another plugin took the name.
202async function registerCommand($: EngineInterface): Promise<void> {
203 try {
204 await $.command.register({ name: 'you-should-know', description: COMMAND_DESCRIPTION, argumentHint: COMMAND_HINT, immediate: true })
205 } catch {
206 try {
207 await $.command.register({ name: 'ysk', description: COMMAND_DESCRIPTION, argumentHint: COMMAND_HINT, immediate: true })
208 } catch {
209 return
210 }
211 }
212}
213
214function clip(text: string, width: number): string {
215 return text.length <= width ? text : `${text.slice(0, Math.max(1, width - 1))}…`
216}
217
218export const register: Register = on => {
219 on('session.start', async ($, e, next) => {
220 const started = await next(e)
221 await registerCommand($)
222 return started
223 })
224
225 on('command.run', { command: ['you-should-know', 'ysk'] }, async ($, e) => {
226 const args = e.args.trim()
227 if (args.toLowerCase() === 'clear') {
228 await update($, noteAtom, () => null)
229 return { text: 'You should know summary cleared.' }
230 }
231 if (await read($, checkingAtom)) return { text: 'A /you-should-know check is already running.' }
232 await update($, checkingAtom, () => true)
233 await update($, noteAtom, () => null)
234 startSpinner($, await $.clock.now())
235 void check($, args)
236 return { text: args === '' ? 'Checking what you should know…' : `Checking what you should know about ${args}…` }
237 })
238
239 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
240 const below = await next(e)
241 if (e.props.hasSurvey) return below
242 const isChecking = await read($, checkingAtom)
243 const note = await read($, noteAtom)
244 if (!isChecking && note === null) return below
245
246 const { Box, Button, Text } = $.ui.resolve(e)
247 const columns = Math.max(30, e.props.bodyColumns - 2)
248 const lead = NUM_CELLS + TAG_CELLS
249 const points = note?.points ?? []
250 const focus = note !== null && note.focus !== '' ? ` · ${note.focus}` : ''
251
252 const seconds = isChecking ? Math.max(0, Math.floor(((await $.clock.now()) - checkStartedAt) / 1000)) : 0
253
254 const mine = isChecking ? (
255 <Box flexDirection="row">
256 <Box width={NUM_CELLS} flexShrink={0}>
257 <Text color="claude">{spinnerAt(frame)}</Text>
258 </Box>
259 <Text bold>You should know</Text>
260 <Text dimColor>{checkingText(seconds)}</Text>
261 </Box>
262 ) : (
263 <Box flexDirection="column">
264 <Box>
265 <Text bold>You should know</Text>
266 <Text dimColor>
267 {focus} · {points.length === 0 ? 'all clear' : `${points.length} point${points.length === 1 ? '' : 's'}`}
268 </Text>
269 </Box>
270 {points.length === 0 && <Text dimColor>Nothing worth flagging right now.</Text>}
271 {points.map((point, i) => {
272 const headline = clip(point.headline, columns - lead)
273 const room = columns - lead - headline.length - 3
274 return (
275 // The number and tag sit in fixed-width boxes, not padded text: the
276 // desktop app's font is proportional, so spaces don't line up there.
277 <Box key={`ysk-point-${i}`} flexDirection="row">
278 <Box width={NUM_CELLS} flexShrink={0}>
279 <Text dimColor>{`${i + 1}`}</Text>
280 </Box>
281 <Box width={TAG_CELLS} flexShrink={0}>
282 <Text color={TAG_COLOR[point.tag]} bold>
283 {point.tag}
284 </Text>
285 </Box>
286 <Text bold>{headline}</Text>
287 {point.detail !== '' && room > 8 && <Text dimColor>{` — ${clip(point.detail, room)}`}</Text>}
288 </Box>
289 )
290 })}
291 <Box gap={2}>
292 <Button key="ysk-ok" label="Understood" onPress={() => update($, noteAtom, () => null)} />
293 {points.length > 0 && <Button key="ysk-ask" label="Ask Claude" onPress={() => askClaude($)} />}
294 </Box>
295 </Box>
296 )
297
298 return below ? (
299 <Box flexDirection="column">
300 {mine}
301 {below}
302 </Box>
303 ) : (
304 mine
305 )
306 })
307}
308types/index.d.ts 30 lines1// You Should Know's state contract: the last summary /you-should-know produced and whether a check is running.
2
3/** What kind of point it is; the summary sorts by this order, most urgent first. */
4export type Tag = 'Risk' | 'Verify' | 'To do' | 'Changed' | 'Assumed' | 'Cost' | 'Note'
5
6/** One thing the person should know. */
7export type Point = {
8 tag: Tag
9 /** The point in a few words. */
10 headline: string
11 /** One short line of why or what to do; '' when the headline says it all. */
12 detail: string
13}
14
15/** One finished /you-should-know check. */
16export type Note = {
17 /** What the side agent found, most urgent first; empty when it found nothing. */
18 points: Point[]
19 /** The focus the person typed after /you-should-know, or '' for none. */
20 focus: string
21 /** When the check finished (ms since epoch). */
22 at: number
23}
24
25declare module 'claude-code' {
26 interface PluginState {
27 'you-should-know': { note: Note | null; isChecking: boolean }
28 }
29}
30