SLOPSHOPPER

qa-only

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

newbandguardcommandprompt
v0.1.0no licenseupdated 2026-10-08cziorvis-commits/qa-only
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · qa-only
› fix the failing auth test and add an audit log call ⏺ 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 › /qa-only ⎿ qa-only: QA-only auto-detect is off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

qa-only

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.

Install

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.

Source 3 files
hooks/register.tsx 71 lines
1import { 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}
71
hooks/detect.ts 11 lines
1// "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}
11
types/index.d.ts 8 lines
1export type QaFlag = boolean
2
3declare module 'claude-code' {
4  interface PluginState {
5    'qa-only': { isActive: QaFlag; isEnabled: QaFlag }
6  }
7}
8