Understand what you ship: plan with a real diagram and decisions you make yourself, then get grilled on it after building.

A Claude Code plugin marketplace.
/plugin marketplace add spiiritual/claude-code-stuff
/plugin install better-compaction@claude-code-stuff
A Claude Mod that fixes two things about compaction:
medium). The model stays the same, so the prompt cache is kept. Subagents running during a summary keep their own effort.Mods need function hooks on. Add this to ~/.claude/settings.json:
{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1" } }
Settings, in /config:
| Option | Default | |
|---|---|---|
deferCompaction | on | Wait for the turn to end before compacting |
deferUnderPercent | 80 | Stop waiting once the context is this full (% of the window) |
compactionEffort | medium | inherit, low, medium, high, xhigh or max |
Measured on a 214k-token session (3 runs each, recall quiz of 22 facts):
| Effort | Summary time | Quiz |
|---|---|---|
| low | 31s | 20.2 |
| medium | 46s | 20.8 |
| high | 59s | 20.7 |
| xhigh | 76s | 19.8 |
| max | 164s | 20.8 |
Recall was the same within noise at every level, while time grew with effort.
In headless mode (-p, SDK) the deferred compaction runs at the start of the next prompt instead of right after the turn.
/plugin install whiteboard-defense@claude-code-stuff
For Mitchell Hashimoto's whiteboard defense: you should be able to explain any customer-facing system you ship and defend its decisions, even if Claude wrote the code.
/whiteboard-defense plan replaces the written plan. Claude draws the system as a real diagram, asks you the decisions that matter as consequence stories ("server dies mid-refund: customer charged with no refund, or refund 30s late?"), then has you explain it back./whiteboard-defense grill quizzes you after building: why X over Y, what a malicious user can do, where it fails. Answers are graded against the code, and drift from the plan is flagged.Plans are saved as cards in the plugin's data folder, never in your repo.
The diagrams are pictures drawn in the transcript by a mod, so they need function hooks on (see above), macOS (it renders with the built-in qlmanage), and a terminal that shows images (Ghostty, kitty, iTerm2, WezTerm; not tmux). Anywhere else you get a text diagram.
hooks/register.tsx 98 lines1import type { Register } from 'claude-code'
2
3const TOOL = 'mcp__whiteboard-defense__draw'
4
5// Shared look, so the model writes only shapes: classes instead of styling.
6const STYLE = `
7 text { font-family: Helvetica, Arial, sans-serif; font-size: 16px; text-anchor: middle; dominant-baseline: central; fill: #222 }
8 .box { fill: #eef; stroke: #335; stroke-width: 1.5 }
9 .edge { fill: none; stroke: #335; stroke-width: 2; marker-end: url(#arrow) }
10 .async { stroke: #c63; stroke-dasharray: 6 4 }
11 .label { font-size: 13px; fill: #555 }
12 .badge circle { fill: #f93 } .badge text { fill: #fff; font-size: 14px }
13`
14const ARROW = '<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0L10,5L0,10z" fill="#335"/></marker>'
15
16const DESCRIPTION = `Draws a diagram as a picture in the user's terminal, and shows it to you too so you can check it. Pass the SVG *body* (no <svg> root) and the canvas size; keep it under ~7 boxes.
17Classes provided: rect.box, path.edge (arrow at the end; add class "async" for dashed orange: background/external calls), text.label (small grey edge label), g.badge (a numbered orange circle: <g class="badge" transform="translate(x,y)"><circle r="13"/><text>1</text></g>). Text is centered on its x,y.
18alt: the same diagram as plain text, shown where pictures can't be.`
19
20export const register: Register = (on) => {
21 on('session.start', async ($, e, next) => {
22 await $.tool.register({
23 name: 'draw',
24 description: DESCRIPTION,
25 inputSchema: {
26 type: 'object',
27 properties: {
28 width: { type: 'number', description: 'Canvas width in px, at most 1000' },
29 height: { type: 'number', description: 'Canvas height in px, at most 1000' },
30 body: { type: 'string', description: 'SVG elements to draw, without the <svg> root' },
31 alt: { type: 'string', description: 'The diagram as plain text' },
32 },
33 required: ['width', 'height', 'body', 'alt'],
34 },
35 })
36 return next(e)
37 })
38
39 // Answers the call itself: SVG -> PNG with macOS's own renderer.
40 on('tool.call', { tool: TOOL }, async ($, e) => {
41 const width = Math.round(Number(e.width)), height = Math.round(Number(e.height))
42 if (!(width > 0 && width <= 1000 && height > 0 && height <= 1000)) {
43 return { deny: 'width and height must be 1 to 1000' }
44 }
45 // qlmanage renders into a square with the picture at the top; centered in
46 // the square instead, sips' center crop takes exactly the picture back.
47 const side = Math.max(width, height)
48 const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${side}" height="${side}" viewBox="0 0 ${side} ${side}">`
49 + `<style>${STYLE}</style><defs>${ARROW}</defs><rect width="${side}" height="${side}" fill="#fff"/>`
50 + `<g transform="translate(${(side - width) / 2},${(side - height) / 2})">${e.body}</g></svg>`
51
52 try {
53 const dir = (await $.process.run(['mktemp', '-d'])).stdout.trim()
54 const file = `${dir}/board.svg`
55 await $.fs.write(file, svg)
56 await $.process.run(['qlmanage', '-t', '-s', String(side * 2), '-o', dir, file])
57 const png = `${file}.png`
58 const crop = await $.process.run(['sips', '-c', String(height * 2), String(width * 2), png])
59 if (crop.exitCode !== 0) throw new Error(crop.stderr)
60 const { base64 } = await $.fs.read(png, { as: 'bytes' })
61 return {
62 result: [
63 // Messages API shape: core hands these blocks to the model as they are.
64 { type: 'image', source: { type: 'base64', media_type: 'image/png', data: base64 } },
65 { type: 'text', text: `Drawn. Check it for arrows that miss their box or overlapping text; call again to fix.\n\n${e.alt}` },
66 ],
67 }
68 } catch (err) {
69 // ponytail: macOS only (qlmanage, sips); a cross-platform renderer if others need it
70 return { result: `Could not draw (${err}). Show the user this text diagram instead:\n\n${e.alt}` }
71 }
72 })
73
74 on('ui.render', { component: 'ToolResult' }, ($, e, next) => {
75 if (e.props.tool !== TOOL || e.surface !== 'terminal') return next(e)
76 const out = e.props.output as any
77 const blocks: any[] = Array.isArray(out) ? out : out?.content ?? []
78 const data: string | undefined = blocks.find((b) => b?.type === 'image')?.source?.data
79 const alt: string = blocks.find((b) => b?.type === 'text')?.text ?? ' '
80 if (!data) return next(e)
81
82 // PNG header: width and height are big-endian at bytes 16 and 20.
83 const head = atob(data.slice(0, 32))
84 const u32 = (at: number) => ((head.charCodeAt(at) << 24) | (head.charCodeAt(at + 1) << 16) | (head.charCodeAt(at + 2) << 8) | head.charCodeAt(at + 3)) >>> 0
85 const { columns, rows } = fit(u32(16), u32(20), e.viewport?.columns ?? 80)
86 const { Image } = $.ui.resolve(e)
87 return <Image source={{ png: data }} columns={columns} rows={rows} alt={alt} />
88 })
89}
90
91// The picture's size in terminal cells.
92function fit(pxWidth: number, pxHeight: number, termColumns: number) {
93 const columns = Math.min(termColumns - 4, 110)
94 // A cell is about twice as tall as it is wide.
95 const rows = Math.round((columns * pxHeight) / pxWidth / 2)
96 return { columns: Math.max(1, Math.min(columns, 255)), rows: Math.max(1, Math.min(rows, 255)) }
97}
98