[FEATURE] AskUserQuestion: Distinguish empty answers from actual user responses in tool_result
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:
- User intentionally submitted empty input (pressed Enter with no selection)
- Permission component failed to inject answers (system bug — see #29962, #30216)
- 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:
- Add
_meta.userRespondedboolean — set totrueby the permission component only when the UI was actually rendered and the user submitted a response - Differentiate empty-but-intentional from missing-due-to-bug — different messages for each case
- 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
- Claude calls
AskUserQuestionwith options: "Which database? [PostgreSQL] [MySQL] [Skip]" - Due to
allowed-toolsauto-approve (#29962),--dangerously-skip-permissions(#30216), or a UI race condition (#30207), the answer is not captured - LLM receives:
"User has answered your questions: " - LLM interprets this as "user chose to proceed without specifying" and picks a default
- 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
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗