SLOPSHOPPER

you-should-know

/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

newbandcommandtoastmodeltimer
v0.1.3MITupdated 2026-10-11meatball-cat/my-mods/you-should-know
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · you-should-know
› fix the failing auth test and add an audit log call ● you-should-know: You should know (1) 1. Note OK ⏺ 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 › /you-should-know ⎿ you-should-know: Checking what you should know… ⠋ You should know · checking… 0s ⟨Claude Code's own drawing⟩ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⠋ You should know · checking… 0s ⟨Claude Code's own drawing⟩
README

you-should-know

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.

CommandWhat it does
/you-should-knowCheck now
/you-should-know <focus>Check with a focus, e.g. /you-should-know tests
/you-should-know clearHide 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
TagMeaning
RiskCould break, lose data or mislead
VerifyLooks done but isn't confirmed
To doLeft unfinished
ChangedTouched outside the ask
AssumedDecided without saying so
CostTime, money or tokens

The summary shows above the prompt with two buttons:

  • Understood hides it.
  • Ask Claude puts the points in the prompt box as a draft, so you choose whether the main chat sees them.

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.

Source 2 files
hooks/register.tsx 308 lines
1// 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}
308
types/index.d.ts 30 lines
1// 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