[FEATURE] AskUserQuestion: Distinguish empty answers from actual user responses in tool_result

Resolved 💬 3 comments Opened Mar 3, 2026 by hubtwork Closed Mar 6, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

The AskUserQuestion tool has a structural issue where the LLM cannot distinguish between these three scenarios:

  1. User intentionally submitted empty input (pressed Enter with no selection)
  2. Permission component failed to inject answers (system bug — see #29962, #30216)
  3. User selected an option but the value wasn't captured (see #30207)

All three cases produce the same tool_result string:

"User has answered your questions: "

This is the root cause behind multiple reported bugs: #29962, #30207, #30216.

Why this happens

The mapToolResultToToolResultBlockParam method formats the result as:

`User has answered your questions: ${
  Object.entries(answers).map(([q, a]) => `"${q}"="${a}"`).join(", ")
}`

When answers is {} (empty object), Object.entries({}) returns [], producing:

"User has answered your questions: "

The LLM sees the word "answered" — a completion signal — and proceeds to the next step, interpreting the empty response as implicit consent or a deliberate skip.

Contributing factors

| Layer | Issue |
|-------|-------|
| Schema | answers is optional in inputSchema — LLM can generate tool_use without answers, passing schema validation |
| call() handler | answers = {} default — missing answers silently become an empty object |
| tool_result format | Completion-signaling language ("answered") regardless of whether answers exist |
| LLM interpretation | "User has answered" → interaction complete → proceed |

Proposed Solution

Add metadata to the tool_result that explicitly distinguishes the three cases:

mapToolResultToToolResultBlockParam({ answers, _meta }, toolUseId) {
  const entries = Object.entries(answers);

  if (!_meta?.userResponded) {
    // Permission component did not collect a response (bug/skip/auto-approve)
    return {
      type: "tool_result",
      content: "ERROR: User response was not collected. Do NOT proceed. Ask the question again using a new AskUserQuestion call.",
      tool_use_id: toolUseId
    };
  }

  if (entries.length === 0) {
    // User explicitly submitted without selecting anything
    return {
      type: "tool_result",
      content: "User saw the questions but submitted without providing any answers. Ask if they want to skip or re-answer.",
      tool_use_id: toolUseId
    };
  }

  // Normal case
  return {
    type: "tool_result",
    content: `User has answered your questions: ${
      entries.map(([q, a]) => `"${q}"="${a}"`).join(", ")
    }`,
    tool_use_id: toolUseId
  };
}

Key changes:

  1. Add _meta.userResponded boolean — set to true by the permission component only when the UI was actually rendered and the user submitted a response
  2. Differentiate empty-but-intentional from missing-due-to-bug — different messages for each case
  3. Use error/instruction language for the bug case — "ERROR: ... Do NOT proceed" is unambiguous to the LLM

Simpler alternative (minimal change)

If adding _meta is too invasive, at minimum change the empty-answers message:

if (entries.length === 0) {
  return {
    type: "tool_result",
    content: "WARNING: No answers were collected from the user. This may indicate a bug. Do not assume user consent. Ask the question again.",
    tool_use_id: toolUseId
  };
}

This alone would fix the most common symptom (LLM skipping on empty answers) without requiring changes to the permission flow.

Alternative Solutions

Currently, users work around this by adding prompt-level rules in CLAUDE.md:

If AskUserQuestion returns empty answers, do not proceed — ask again.

This is fragile because:

  • It competes with the "User has answered" signal in the tool_result
  • It cannot distinguish intentional empty input from system bugs
  • Every user must independently discover and add this workaround

Priority

High - Significant impact on productivity

Feature Category

API and model interactions

Use Case Example

  1. Claude calls AskUserQuestion with options: "Which database? [PostgreSQL] [MySQL] [Skip]"
  2. Due to allowed-tools auto-approve (#29962), --dangerously-skip-permissions (#30216), or a UI race condition (#30207), the answer is not captured
  3. LLM receives: "User has answered your questions: "
  4. LLM interprets this as "user chose to proceed without specifying" and picks a default
  5. User never actually made a choice, but Claude is already executing

Additional Context

Related Issues

  • #29962 — AskUserQuestion auto-completes with empty answers when in allowed-tools
  • #30207 — AskUserQuestion frequently returns blank/empty answers
  • #30216 — AskUserQuestion regression with --dangerously-skip-permissions
  • #9846 — Original report thread

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗