Claude asks for your call in the built-in question box, with an everyday story and a recommendation for every option.

Mods for the Claude Code desktop app and terminal.
When Claude needs your call, it asks in Claude Code's own question box instead of in plain text, and it writes every option the same way:
In the desktop app the stories sit in a panel just above the box, so you can compare all the options at once. In the terminal, your pick shows above the box and each story shows when you move to that option. You click an option, or type your own answer under Other, and Claude carries on with it right away.
The mod also tells Claude to use the box whenever it needs your input, and to hand questions up to the main agent when a subagent hits one.
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace, enter screamyx/claude-code-mods, then Add plugin and choose decision-box.
At the prompt of a terminal session:
/plugin install decision-box --marketplace screamyx/claude-code-mods
From a shell:
claude plugin install decision-box --marketplace screamyx/claude-code-mods
Answer y to add the marketplace, then pick the user scope so it applies to every session.
If this repository is private, your machine needs a git login for github.com first (gh auth login, then gh auth setup-git).
claude plugin validate decision-box
claude plugin test decision-boxhooks/register.tsx 82 lines1import type { Register } from 'claude-code'
2
3import { panelFor, withoutStories } from './stories'
4
5export const RULES = `# Asking the person to decide (decision-box mod)
6
7Whenever you need the person's input to go on, ask with the AskUserQuestion tool, not in plain text. That covers a choice between approaches, a call on direction, a confirmation before a step that is hard to undo, and a clarifying question about what they expect. Use it whenever you can: the person would rather click an answer than type one. Do not ask about things you can reasonably decide yourself, and never ask the same thing twice.
8
9Write every question like this:
10- question: plain words, no jargon, ending in "?".
11- Always recommend one option. Put it first and end its label with " (Recommended)".
12- label: 1 to 5 words. A technical term is fine as a label, but the scenario below comes first in the person's mind.
13- description: one short line on what happens if it is picked.
14- preview: the everyday scenario, exactly in these lines:
15 Normal day: who is affected (a staff member, a customer, the person themselves), and what they see or do.
16 Bad case: what goes wrong, concretely.
17 and on the recommended option only:
18 Why: one sentence on why you recommend it.
19- multiSelect only when picks really combine.
20- No "Other" option: the box already has a field for typing.
21
22When the answer comes back, act on it straight away. Free text the person typed wins over the options you offered.
23
24Subagents cannot reach the person. If you are a subagent, hand the question back to the agent that started you, written the same way.`
25
26const TOOL_NOTE = `
27
28Write the options as the decision-box rules in the system prompt say: the recommended option first with " (Recommended)" in its label, and each option's preview holding "Normal day:" and "Bad case:" lines, plus "Why:" on the recommended one.`
29
30export const register: Register = on => {
31 on('prompt.compose', async ($, e, next) => {
32 const composed = await next(e)
33 if (!e.tools.includes('AskUserQuestion')) return composed
34
35 return {
36 sections: [
37 ...composed.sections,
38 { id: 'decision-box:rules', text: RULES, scope: 'session' },
39 ],
40 }
41 })
42
43 on('tool.describe', { tool: 'AskUserQuestion' }, async ($, e, next) => {
44 const described = await next(e)
45
46 return { ...described, description: described.description + TOOL_NOTE }
47 })
48
49 on('ui.render', { component: 'AskUserQuestion' }, async ($, e, next) => {
50 const panel = panelFor(e.props.questions, e.surface)
51 if (panel === undefined) return next(e)
52
53 const box = await next(
54 panel.isStripping
55 ? { ...e, props: { ...e.props, questions: withoutStories(e.props.questions) } }
56 : e,
57 )
58
59 if (e.surface === 'terminal') {
60 const { Box, Text } = $.ui.resolve(e)
61
62 return (
63 <Box flexDirection="column" gap={1}>
64 <Text dimColor>{panel.text}</Text>
65 {box}
66 </Box>
67 )
68 }
69
70 const { Box, Markdown } = $.ui.resolve(e)
71
72 return (
73 <Box flexDirection="column" gap="sm">
74 <Box flexDirection="column" padding="sm" borderStyle="round" borderDimColor>
75 <Markdown text={panel.text} />
76 </Box>
77 {box}
78 </Box>
79 )
80 })
81}
82hooks/stories.ts 176 lines1// Reads the everyday story Claude writes into each option's `preview`:
2// Normal day: ...
3// Bad case: ...
4// Why: ... (the recommended option only)
5
6export type Story = {
7 label: string
8 isRecommended: boolean
9 day?: string
10 bad?: string
11 why?: string
12}
13
14export type Asked = {
15 header: string
16 question: string
17 stories: Story[]
18}
19
20type RawOption = { label?: unknown; description?: unknown; preview?: unknown }
21type RawQuestion = { question?: unknown; header?: unknown; options?: unknown }
22
23const RECOMMENDED = /\s*\((recommended|my pick)\)\s*$/i
24const LINE = /^\s*(normal day|bad case|why)\s*:\s*(.+)$/i
25
26const text = (value: unknown): string => (typeof value === 'string' ? value : '')
27
28export function cleanLabel(label: string): string {
29 return label.replace(RECOMMENDED, '')
30}
31
32export function readStory(option: RawOption): Story {
33 const label = text(option.label)
34 const story: Story = {
35 label: cleanLabel(label),
36 isRecommended: RECOMMENDED.test(label),
37 }
38
39 for (const line of text(option.preview).split('\n')) {
40 const match = LINE.exec(line)
41 if (match === null) continue
42 const kind = (match[1] ?? '').toLowerCase()
43 const body = (match[2] ?? '').trim()
44 if (kind === 'normal day') story.day = body
45 else if (kind === 'bad case') story.bad = body
46 else story.why = body
47 }
48 if (story.why !== undefined) story.isRecommended = true
49
50 return story
51}
52
53export function hasStory(story: Story): boolean {
54 return story.day !== undefined || story.bad !== undefined
55}
56
57export function readQuestions(questions: readonly unknown[]): Asked[] {
58 return questions.map(raw => {
59 const q = (raw ?? {}) as RawQuestion
60 const options = Array.isArray(q.options) ? (q.options as RawOption[]) : []
61
62 return {
63 header: text(q.header),
64 question: text(q.question),
65 stories: options.map(readStory),
66 }
67 })
68}
69
70// The previews the mod draws itself come off the options, so the box does
71// not show the same story twice. Previews that hold no story stay.
72export function withoutStories(questions: readonly unknown[]): unknown[] {
73 return questions.map(raw => {
74 const q = (raw ?? {}) as RawQuestion
75 if (!Array.isArray(q.options)) return raw
76
77 return {
78 ...q,
79 options: (q.options as RawOption[]).map(option => {
80 if (!hasStory(readStory(option))) return option
81 const { preview: _, ...rest } = option
82
83 return rest
84 }),
85 }
86 })
87}
88
89export function pickLine(asked: Asked): string | undefined {
90 const pick = asked.stories.find(story => story.isRecommended)
91 if (pick === undefined) return undefined
92
93 return pick.why === undefined
94 ? `My pick: ${pick.label}.`
95 : `My pick: ${pick.label}. ${pick.why}`
96}
97
98export function storyMarkdown(asked: readonly Asked[]): string {
99 const blocks: string[] = []
100
101 for (const one of asked) {
102 if (!one.stories.some(hasStory)) continue
103 const lines: string[] = []
104 if (asked.length > 1 && one.header !== '') lines.push(`**${one.header}**`)
105 const pick = pickLine(one)
106 if (pick !== undefined) lines.push(pick, '')
107 for (const story of one.stories) {
108 if (!hasStory(story)) continue
109 lines.push(`**${story.label}**`)
110 if (story.day !== undefined) lines.push(`Normal day: ${story.day}`)
111 if (story.bad !== undefined) lines.push(`*Bad case:* ${story.bad}`)
112 lines.push('')
113 }
114 blocks.push(lines.join('\n').trim())
115 }
116
117 return blocks.join('\n\n---\n\n')
118}
119
120// Rows as the question dialog counts them: one per line, one more for every
121// 40 columns a line runs to.
122export function countRows(markdown: string): number {
123 return markdown
124 .split('\n')
125 .reduce((rows, line) => rows + Math.max(1, Math.ceil(line.length / 40)), 0)
126}
127
128export function picksMarkdown(asked: readonly Asked[]): string {
129 return asked
130 .map(one => {
131 const pick = pickLine(one)
132 if (pick === undefined) return undefined
133
134 return asked.length > 1 && one.header !== '' ? `**${one.header}:** ${pick}` : pick
135 })
136 .filter((line): line is string => line !== undefined)
137 .join('\n\n')
138}
139
140// The desktop, VS Code and mobile frame above the box holds 48 rows; a few
141// are kept spare for the gap and wrapping the count cannot see.
142const ROWS = 42
143// The terminal frame holds 12 and draws no Markdown.
144const TERMINAL_ROWS = 4
145
146export type Panel = {
147 // What goes above the box.
148 text: string
149 // Whether the stories come off the options' previews, shown above instead.
150 isStripping: boolean
151}
152
153export function panelFor(questions: readonly unknown[], surface: string): Panel | undefined {
154 const asked = readQuestions(questions)
155
156 if (surface === 'terminal') {
157 // The terminal shows each option's preview as the box's own, so the
158 // stories stay there and only the picks go above.
159 const picks = picksMarkdown(asked).replace(/\*\*/g, '')
160 if (picks === '' || countRows(picks) > TERMINAL_ROWS) return undefined
161
162 return { text: picks, isStripping: false }
163 }
164
165 const story = storyMarkdown(asked)
166 if (story === '') return undefined
167 if (countRows(story) <= ROWS) return { text: story, isStripping: true }
168
169 // Too long to show whole: the picks go above and each story stays in its
170 // option's preview, where the box shows it on focus.
171 const picks = picksMarkdown(asked)
172 if (picks === '' || countRows(picks) > ROWS) return undefined
173
174 return { text: picks, isStripping: false }
175}
176