Prompts that start with question / Q / q get an answer only: no plans, no edits. A ? pill shows when it is on.

A Claude Code mod. Start a prompt with question, Q or q (e.g. Q how does claude work) and Claude answers in chat only: no plans, no file edits. A ? pill at the right of the row above the prompt turns green on those turns and grey otherwise.
/qa-only on / /qa-only off / /qa-only (toggle). Off hides the pill. The choice is remembered.In a terminal Claude Code session:
/plugin install qa-only --marketplace cziorvis-commits/qa-only
Answer y to add the marketplace, then pick the user scope. It then loads in every session, including the desktop app's Code tab.
hooks/register.tsx 71 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import { isQuestion } from './detect'
5
6const isActive = atom({ plugin: 'qa-only', key: 'isActive' } as const, false)
7const isEnabled = atom({ plugin: 'qa-only', key: 'isEnabled' } as const, true)
8
9// Tools that change things or start a plan; reading and searching stay allowed.
10const BLOCKED = new Set(['Edit', 'Write', 'NotebookEdit', 'EnterPlanMode', 'ExitPlanMode'])
11
12const CONTEXT = [
13 'QA-only mode: the user is asking a question and wants a chat answer, not work done.',
14 'Answer directly in conversation. Do not edit or create files, run commands that change state,',
15 'write a coding plan, or start implementing. Reading files or searching to inform the answer is fine.',
16 'If the answer implies a change, describe it briefly and stop; the user will ask for it explicitly.',
17].join(' ')
18
19export const register: Register = on => {
20 on('session.start', async ($, e, next) => {
21 const stored = await $.store.get('isEnabled')
22 if (typeof stored === 'boolean') await update($, isEnabled, () => stored)
23 await $.command.register({
24 name: 'qa-only',
25 description: 'Turn QA-only auto-detect on or off (on | off | toggle)',
26 })
27
28 return next(e)
29 })
30
31 on('command.run', { command: 'qa-only' }, async ($, e) => {
32 const word = e.args.trim().toLowerCase()
33 const current = await read($, isEnabled)
34 const wanted = word === 'on' ? true : word === 'off' ? false : !current
35 await update($, isEnabled, () => wanted)
36 await $.store.set('isEnabled', wanted)
37 if (!wanted) await update($, isActive, () => false)
38
39 return { text: `QA-only auto-detect is ${wanted ? 'on' : 'off'}.` }
40 })
41
42 on('prompt.submit', async ($, e, next) => {
43 const isQa = (await read($, isEnabled)) && isQuestion(e.text)
44 await update($, isActive, () => isQa)
45
46 return isQa ? next({ ...e, context: [...(e.context ?? []), CONTEXT] }) : next(e)
47 }).catch(($, e, next) => next(e))
48
49 on('tool.call', async ($, e, next) => {
50 if (BLOCKED.has(e.tool) && (await read($, isActive))) {
51 return { deny: `${$.plugin.name}: this was a question, so no edits or plans. Answer in chat instead.` }
52 }
53
54 return next(e)
55 }).catch(($, e, next) => next(e))
56
57 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
58 if (e.props.hasSurvey || !(await read($, isEnabled))) return next(e)
59 const { Box, Text } = $.ui.resolve(e)
60 const active = await read($, isActive)
61
62 return (
63 <Box flexDirection="row" justifyContent="flex-end" width="100%">
64 <Text color="black" backgroundColor={active ? 'green' : 'gray'} bold>
65 {' ? '}
66 </Text>
67 </Box>
68 )
69 })
70}
71hooks/detect.ts 11 lines1// "question …", "Q …", "q: …" at the start of the first line (not "quick", "query").
2// Slash commands are never questions.
3const PREFIX = /^(question|q)(?![a-z0-9])/i
4
5export const isQuestion = (text: string): boolean => {
6 const trimmed = text.trim()
7 if (trimmed === '' || trimmed.startsWith('/')) return false
8
9 return PREFIX.test(trimmed.split('\n', 1)[0].trim())
10}
11types/index.d.ts 8 lines1export type QaFlag = boolean
2
3declare module 'claude-code' {
4 interface PluginState {
5 'qa-only': { isActive: QaFlag; isEnabled: QaFlag }
6 }
7}
8