Offers to turn a long, loose prompt into a brief (Goal, Context, Limits, Done when), with up to three questions on what it leaves open.

A Claude Code mod that turns a long, loose prompt into a clear brief before you send it.
When you send a prompt of 150 words or more, brain-dump asks: "This prompt is long. Make a brief from it first?"
Nothing goes to Claude until you press Enter on the brief. If brain-dump can't make a brief, it puts your prompt back in the box.
It asks only for prompts that you type or send from the Remote Control app. Slash commands, prompts sent while Claude works, and prompts from other mods pass as they are. A brief that brain-dump made is not asked about again.
Each brief uses two Sonnet calls on your account: one for the questions and one for the brief.
In a Claude Code terminal session:
/plugin install brain-dump --marketplace whykothari/whystack
To run a local copy instead, see the mods README.
Inspired by a mod idea from Ben AI on YouTube. The code is new and written for whystack.
hooks/register.ts 127 lines1import type { EngineInterface, PromptSubmitInput, Register } from 'claude-code'
2
3// A long prompt typed in one go (often dictated) gets an offer before it is
4// sent: a few questions on what it leaves open, then a brief in four parts in
5// the prompt box. The person reads the brief and sends it with Enter.
6
7const MIN_WORDS = 150
8const MODEL = 'sonnet'
9const MAKE = 'Make a brief'
10const SEND = 'Send it now'
11const SECTIONS = ['Goal', 'Context', 'Limits', 'Done when'] as const
12// A brief this mod wrote ends with this section, so sending it asks nothing.
13const LAST_HEADING = `## ${SECTIONS[SECTIONS.length - 1]}`
14
15const QUESTIONS_SYSTEM = `You read a long, loose request that a person wrote for an AI coding agent.
16Find at most 3 points where the request is unclear, leaves a choice open, or contradicts itself, and where the answer changes the work.
17Skip points the agent can find out by itself from the code or the files.
18For each point, write one short question and 2 to 4 short answers the person is likely to pick.
19Reply with JSON only, no other text, in this shape:
20{"questions":[{"question":"...?","header":"max 12 chars","options":["...","..."]}]}
21If nothing important is unclear, reply {"questions":[]}.`
22
23const BRIEF_SYSTEM = `You turn a long, loose request into a clear brief for an AI coding agent.
24Keep every fact, name, path and number the person gave. Add no new requirements.
25Use the person's answers to settle the open points.
26Write plain, short sentences. Use exactly these four Markdown headings, in this order:
27${SECTIONS.map(s => `## ${s}`).join('\n')}
28Goal: what to do, in one to three sentences.
29Context: why it matters and what the agent must know.
30Limits: what not to touch, what to keep, what to ask about first.
31Done when: checks that show the work is finished.
32Reply with the brief only.`
33
34type Question = { question: string; header?: string; options: string[] }
35
36function wordCount(text: string): number {
37 return text.split(/\s+/).filter(Boolean).length
38}
39
40function isCandidate(e: PromptSubmitInput): boolean {
41 return (
42 (e.origin?.kind === 'composer' || e.origin?.kind === 'bridge') &&
43 e.command === undefined &&
44 e.isMeta === undefined &&
45 e.turnId === undefined &&
46 wordCount(e.text) >= MIN_WORDS &&
47 !e.text.includes(LAST_HEADING)
48 )
49}
50
51/** The model's questions, cut to what the dialog takes; none when the reply can't be read. */
52function parseQuestions(reply: string): Question[] {
53 const json = reply.slice(reply.indexOf('{'), reply.lastIndexOf('}') + 1)
54 try {
55 const list: unknown = JSON.parse(json).questions
56 if (!Array.isArray(list)) return []
57 return list
58 .filter((q): q is Question => typeof q?.question === 'string' && Array.isArray(q.options))
59 .slice(0, 3)
60 .map(q => ({
61 question: q.question.trim().endsWith('?') ? q.question.trim() : `${q.question.trim()}?`,
62 header: typeof q.header === 'string' ? q.header.slice(0, 12) : undefined,
63 options: q.options.filter(o => typeof o === 'string' && o.trim() !== '').slice(0, 4),
64 }))
65 .filter(q => q.options.length >= 2)
66 } catch {
67 return []
68 }
69}
70
71async function askQuestions($: EngineInterface, text: string): Promise<string[]> {
72 const found = await $.model.complete({ model: MODEL, system: QUESTIONS_SYSTEM, prompt: text, maxTokens: 800, timeoutMs: 60_000 })
73 if (!found.isAnswered) return []
74 const answers: string[] = []
75 for (const q of parseQuestions(found.text)) {
76 // A dismissed question is left open; the brief says so in Limits.
77 const answer = await $.ui.ask(q.question, { options: q.options, header: q.header }).catch(() => null)
78 answers.push(`Q: ${q.question}\nA: ${answer ?? '(not answered)'}`)
79 }
80 return answers
81}
82
83async function writeBrief($: EngineInterface, text: string, answers: string[]): Promise<string | null> {
84 const prompt = answers.length === 0 ? text : `${text}\n\nThe person's answers:\n\n${answers.join('\n\n')}`
85 const brief = await $.model.complete({ model: MODEL, system: BRIEF_SYSTEM, prompt, maxTokens: 2000, timeoutMs: 90_000 })
86 return brief.isAnswered && brief.text.includes(LAST_HEADING) ? brief.text.trim() : null
87}
88
89// The box empties when the dropped prompt leaves it, so the fill waits a moment.
90function fillLater($: EngineInterface, text: string, note: string) {
91 void (async () => {
92 await $.clock.sleep(300)
93 const filled = await $.prompt.fill({ text }).catch(() => ({ isFilled: false }))
94 if (filled.isFilled) {
95 $.ui.toast(note)
96 } else {
97 $.ui.log(`brain-dump could not fill the prompt box. The text:\n\n${text}`)
98 }
99 })()
100}
101
102export const register: Register = on => {
103 on('prompt.submit', async ($, e, next) => {
104 if (!isCandidate(e)) return next(e)
105
106 const choice = await $.ui
107 .ask('This prompt is long. Make a brief from it first?', { options: [MAKE, SEND], header: 'Brain dump' })
108 .catch(() => SEND)
109 if (choice !== MAKE) return next(e)
110
111 $.ui.status('brain-dump: reading your prompt')
112 try {
113 const answers = await askQuestions($, e.text)
114 $.ui.status('brain-dump: writing the brief')
115 const brief = await writeBrief($, e.text, answers)
116 if (brief === null) {
117 fillLater($, e.text, 'Could not make a brief. Your prompt is back in the box.')
118 return { drop: 'brain-dump could not make a brief, so it put your prompt back.' }
119 }
120 fillLater($, brief, 'The brief is in the prompt box. Check it, then press Enter.')
121 return { drop: 'brain-dump made a brief from this prompt.' }
122 } finally {
123 $.ui.status(undefined)
124 }
125 }).catch(($, e, next) => next(e))
126}
127