SLOPSHOPPER

ask-choice

When a reply ends with a choice-based question written in plain text, has the model re-ask it with the AskUserQuestion picker so you can select an option…

newguardmodeltimer
v0.1.1MITupdated 2026-10-07i-noma-ru/claude-ask-choice
A shopper browsing a rack in a slop shop
README

claude-ask-choice

A small plugin ("mod") for Claude Code's terminal UI that turns a choice-based question at the end of a reply into a selectable menu. When a reply ends with a question you could answer by picking an option, but it is written in plain text, the plugin has the model re-ask it with the AskUserQuestion tool, so you can select an option instead of typing.

日本語の説明は README.ja.md にあります。

In-app messages are in Japanese.

When to use

  • When Claude ends a reply with a question that has a few clear answers, but writes it as prose and you have to type the answer back.
  • When you work with Claude in Japanese: the question indicators include the Japanese endings ですか, ますか, でしょうか, ましょうか as well as ? / ?.
  • When you want the re-ask to happen only where it is needed: it steps in once per question, and never on a turn that already used AskUserQuestion.

Not for you if you run Claude non-interactively (claude -p), if you do not want one extra classification call for each reply that ends with a question, or if you are on Windows (untested).

What it looks like

After a reply that ends with a choice-based question in prose, a short English message starting with [MOD ask-choice] appears on screen, shown by Claude Code as a message sent by the plugin. The model then re-asks the same question with the AskUserQuestion picker. Replies without a question indicator are left as they are.

Requirements

  • Built on Claude Code's plugin hooks ("mods") API, which is in early access and may change between versions.
  • Developed and tested with Claude Code 2.1.287 to 2.1.289 on macOS.
  • Windows is untested.

Install

From the marketplace in this repository:

claude plugin marketplace add i-noma-ru/claude-ask-choice
claude plugin install ask-choice@claude-ask-choice

Or for one session only, from a clone:

claude --plugin-dir /path/to/claude-ask-choice

To load a clone in every session, add it to the CLAUDE_CODE_PLUGIN_DIRS environment variable: a :-separated list where each entry is either a plugin folder itself or a folder that contains plugin folders (both confirmed in 2.1.292). The variable does not appear in claude --help, so treat it as subject to change.

How it works

At the end of each turn, the plugin inspects the end of the reply (the last 1,500 characters, with code blocks and inline code removed).

  1. If the text shows no sign of a question, nothing happens. Indicators include: a line ending in ? / ? or the Japanese endings ですか, ますか, でしょうか, ましょうか; the phrases 選んでください, 教えてください, 返信してください, 決めてください; or the mark 🚦.
  2. If an indicator is present, the plugin asks the session's model to classify the text into one of three categories: a question answerable by choosing from options, a question requiring a free-form answer, or no question for the user.
  3. Only for the first category, it submits a short English message (starting with [MOD ask-choice]) instructing the model to re-ask with AskUserQuestion.

It does nothing if the turn already used AskUserQuestion, in non-interactive runs (claude -p), for subagent replies, for turns that ended in an abort or error, or on the turn immediately following a bounce (one bounce per question).

Cost: One classification call for each reply with a question indicator, plus one extra model turn for each bounce. Replies without an indicator incur no cost.

The submitted message is visible on screen, shown by Claude Code as a message sent by the plugin.

What the plugin reads and what it submits

This section lists everything the plugin reads from the session and everything it puts into a prompt. Nothing is sent anywhere other than the model of the current Claude Code session, and nothing is stored.

What it reads: the text of the reply that just finished (turn.complete), the turn's end reason, whether the turn belongs to a subagent, whether the session is interactive, and whether AskUserQuestion was called during the turn. It does not read the user's prompts, files, or environment variables.

What it sends to the classifier: the last 1,500 characters of the reply, with fenced code blocks and inline code removed, together with three fixed category labels (written in Japanese in the source). This goes to the session's own model through Claude Code's $.model.classify API.

What it submits as a prompt: one fixed English message, always the same text and never containing any part of the reply or the conversation:

[MOD ask-choice] Your last reply left a pick-one question for the user in prose.
Re-ask it with AskUserQuestion: put the deciding facts and your recommendation in the question text and the option descriptions.
Do not repeat the report body. If the question truly needs a free-form answer, say so in one line.

Tests

claude plugin validate .
claude plugin test .

Notes

  • Written with AI assistance (Claude Code).
  • This plugin comes from the author's own setup. The source comments and test names are in Japanese.
  • Companion plugin: claude-clip-command. The two are independent.

License

MIT. See LICENSE.

Source 2 files
hooks/register.ts 72 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3import { BOUNCE, CHOICE, LABELS, mayAsk, tailOf } from './logic'
4
5// 問いの種類をモデルに判定させ、選んで答えられる問いなら聞き直しを送る
6async function judgeAndBounce($: EngineInterface, answer: string, onBounce: () => void): Promise<void> {
7  const kind = await $.model.classify(tailOf(answer), LABELS, { model: await $.session.model() })
8
9  if (kind !== CHOICE) {
10    return
11  }
12
13  onBounce()
14  await $.prompt.submit({ text: BOUNCE })
15}
16
17export const register: Register = on => {
18  // 画面の無い実行(claude -p)では何もしない
19  let isInteractive = false
20  // このターンで AskUserQuestion が使われた
21  let isAsked = false
22  // 直前に聞き直しを送った(その次のターンは見ない。聞き直しは 1 回だけ)
23  let isBounced = false
24
25  on('session.start', async ($, e, next) => {
26    isInteractive = e.isInteractive
27
28    return next(e)
29  })
30
31  on('turn.start', async ($, e, next) => {
32    isAsked = false
33
34    return next(e)
35  })
36
37  on('tool.call', { tool: 'AskUserQuestion' }, async ($, e, next) => {
38    isAsked = true
39
40    return next(e)
41  })
42
43  on('turn.complete', async ($, e, next) => {
44    const done = await next(e)
45
46    if (!isInteractive || e.agentId !== undefined || e.reason !== 'answer') {
47      return done
48    }
49
50    if (isBounced) {
51      isBounced = false
52
53      return done
54    }
55
56    if (isAsked || !mayAsk(e.answer)) {
57      return done
58    }
59
60    // 判定は返答の表示を待たせないよう、ターンが閉じたあとに行う(フックの中から別の入力は送れない)
61    $.clock.after(50, () => {
62      judgeAndBounce($, e.answer, () => {
63        isBounced = true
64      }).catch(() => {
65        // 判定や送信に失敗しても、返答はそのまま
66      })
67    })
68
69    return done
70  })
71}
72
hooks/logic.ts 37 lines
1// 返答の末尾のうち、問いを探す範囲(文字数)。長い報告の途中にある問いかけの引用を拾わないため
2const TAIL_CHARS = 1500
3
4// 問いの目印: 🚦、行末の疑問、返事を求める言い回し
5const ASK_PATTERNS = [
6  /🚦/,
7  /(?:[??]|ですか|ますか|でしょうか|ましょうか)[」』))。]*\s*$/m,
8  /(?:選んで|教えて|返信して|決めて)ください/,
9]
10
11export const CHOICE = '選択肢から選んで答えられる問いがある'
12export const LABELS = [CHOICE, '自由に書いて答える問いだけがある', '利用者への問いは無い'] as const
13
14/**
15 * 差し戻すときに主担当へ送る文。画面では利用者の発言と同じ見た目で出るので、
16 * 見分けがつくように印つきの英語にする(2026-10-04・利用者は読まなくてよい)
17 */
18export const BOUNCE = [
19  '[MOD ask-choice] Your last reply left a pick-one question for the user in prose.',
20  'Re-ask it with AskUserQuestion: put the deciding facts and your recommendation in the question text and the option descriptions.',
21  'Do not repeat the report body. If the question truly needs a free-form answer, say so in one line.',
22].join('\n')
23
24/** コードブロックと行内コードを除いた、返答の末尾 */
25export function tailOf(answer: string): string {
26  const prose = answer.replace(/```[\s\S]*?(?:```|$)/g, '').replace(/`[^`\n]*`/g, '')
27
28  return prose.slice(-TAIL_CHARS)
29}
30
31/** 返答の末尾に、問いの目印があるか(ここで絞ってから、モデルに種類を判定させる) */
32export function mayAsk(answer: string): boolean {
33  const tail = tailOf(answer)
34
35  return ASK_PATTERNS.some(pattern => pattern.test(tail))
36}
37