Repository navigation
AskUserQuestion silently broken: PreToolUse hook emits permissionDecision:'defer' → 'Tool result missing due to internal error' #2035
Description
Summary
The PreToolUse hook for AskUserQuestion (question-preference-hook) emits permissionDecision: "defer" on its no-op path. Current Claude Code builds choke on that value and abort the tool call, so every AskUserQuestion call fails with:
[Tool result missing due to internal error]
This hits the common case: any AUQ call where no never-ask preference matches falls through to defer(), which is essentially all of them. The result is that the questions UI is silently broken for the user — the call is made, no answer ever comes back.
Environment
- gstack: v1.55.1.0
- Claude Code: recent build (Opus 4.8 era, June 2026), VSCode extension
- OS: macOS (darwin 25.5.0)
bunpresent and on PATH. The hook itself runs cleanly — exit 0, no entries in~/.gstack/hook-errors.log. The problem is the output it emits, not a crash.
Root cause
hosts/claude/hooks/question-preference-hook.ts → defer():
function defer(additionalContext?: string): void {
const out: Record<string, unknown> = {
hookEventName: 'PreToolUse',
permissionDecision: 'defer',
};
if (additionalContext) out.additionalContext = additionalContext;
process.stdout.write(JSON.stringify({ hookSpecificOutput: out }));
process.exit(0);
}defer() is the default/fall-through path for any AUQ call without a matching never-ask preference. On a normal question it emits:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer"}}Empirically, current Claude Code builds reject this permissionDecision: "defer" and abort the tool call with an internal error. (The hooks docs are ambiguous on whether "defer" is an accepted permissionDecision value alongside allow/deny/ask; regardless, emitting it currently breaks the call, whereas the documented "no decision" form — exit 0 with no stdout — works.)
Reproduction
- gstack 1.55.1.0 with the PreToolUse hook registered in
~/.claude/settings.json(matcher: "(AskUserQuestion|mcp__.*__AskUserQuestion)"). - Trigger any
AskUserQuestionthat has no matchingnever-askpreference. - The tool result is
[Tool result missing due to internal error]. 100% reproducible.
Isolation evidence
Neutralizing the shims to exit 0 no-ops pinpoints the PreToolUse hook:
| PreToolUse | PostToolUse | Result |
|---|---|---|
| real | real | ❌ internal error |
| no-op | no-op | ✅ works |
| no-op | real | ✅ works → culprit is PreToolUse |
| fixed (below) | real | ✅ works |
Suggested fix
The documented "no decision / proceed via the normal permission flow" output is exit 0 with no stdout. Only emit JSON when there is actually additionalContext to inject, and without a permissionDecision:
function defer(additionalContext?: string): void {
// "Proceed via the normal permission flow" is exit 0 with no output.
// Emitting permissionDecision:'defer' makes recent Claude Code builds reject
// the tool call with an internal error, silently breaking every AUQ call.
if (additionalContext) {
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext,
},
}),
);
}
process.exit(0);
}Verified locally: with this change, AskUserQuestion works with both gstack hooks active (PreToolUse + PostToolUse).
The deny() path is unaffected — permissionDecision: "deny" is a standard value and works fine; only the defer() no-op path needs changing.
Activity
hotsauce0817 commentedon Jun 23, 2026
Reproduced on plain Claude for Desktop 1.14271.0 (bundled Claude Code 2.1.181), no Conductor involved —
CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL=true. EveryAskUserQuestioncall returns[Tool result missing due to internal error]and the picker never renders.Isolated to a single variable: with the PreToolUse
question-preference-hookemittingpermissionDecision:"defer"→ broken; with it emitting nothing (exit 0) → works. So this isn't Conductor-specific — the mainstream Desktop host is also strict about the unknowndefervalue. Fixed by #1924 / #1816; flagging the broader blast radius as a reason to prioritize the merge.optimization2026 commentedon Jun 30, 2026
This looks like a tool permission decision contract issue.
AskUserQuestionis not an ordinary tool call. It is a control-flow tool that asks the user for required input.If a PreToolUse hook emits
permissionDecision: "defer"but the runtime interprets that as an internal missing tool result, the user-facing question path breaks.A standard AISP-style contract could make the expected states explicit:
[ { "role": "system", "content": { "protocol": "AISP V1.0.0", "axiom_0": "Human_Sovereignty_and_Wellbeing", "id": "gstack_ask_user_question_permission_contract_aisp", "name": "gstack AskUserQuestion Permission Contract", "version": "1.0.0", "license": "MIT", "summary": "Machine-checkable contract for AskUserQuestion pre-tool permission decisions.", "flow_format": "mermaid", "loading_mode": "node", "tools": ["user_interaction"], "params": { "tool_name": "AskUserQuestion", "hook_decision": "string" }, "system_prompt": "" } }, { "role": "user", "content": { "instruction": "STRICTLY OBEY aisp_contract; its non_negotiable rules are inviolable; then RUN aisop.main", "user_input": "{user_input}", "aisp_contract": { "profile": "aisp.skill.v1", "invocation": { "mode": "tool_permission_policy", "when_to_use": [ "before AskUserQuestion tool execution", "when PreToolUse hook returns allow, deny, or defer", "when user clarification is required" ], "when_not_to_use": [ "non-interactive execution", "tools that do not involve user question cards" ] }, "non_negotiable": [ { "rule": "AskUserQuestion defer must route to the user-question card flow, not internal missing result.", "enforced_by": "route.step2:sys.assert" }, { "rule": "Every permission decision must map to a valid runtime state.", "enforced_by": "validate.step2:sys.assert" } ], "discovery": { "category": "tool_permission", "tags": ["gstack", "AskUserQuestion", "PreToolUse", "defer", "aisp"] }, "risk_level": "medium", "resources": [] }, "aisop": { "main": "graph TD\n read[Read hook decision] --> validate[Validate decision state]\n validate --> route[Route AskUserQuestion behavior]\n route --> report[Emit tool-state trace]\n report --> end_node((End))" }, "functions": { "read": { "step1": "Read tool_name and hook_decision.", "output_mapping": "permission_input" }, "validate": { "step1": "Validate hook_decision is allow, deny, defer, or ask_user.", "step2": "sys.assert('hook decision maps to valid runtime state', 'Invalid or unhandled permission decision')", "output_mapping": "permission_validation" }, "route": { "step1": "If tool_name is AskUserQuestion and hook_decision is defer, route to user-question card flow.", "step2": "sys.assert('AskUserQuestion defer maps to user interaction flow', 'AskUserQuestion defer must not become missing tool result')", "output_mapping": "tool_route" }, "report": { "step1": "Record tool name, hook decision, routed state, and result.", "output_mapping": "tool_permission_trace" }, "end_node": { "step1": "Return tool_permission_trace." } } } } ]The implementation fix is likely small:
The invariant:
time-attack commentedon Jul 15, 2026
@16francej — recommend closing this issue only after a current-source fix derived from #1924 passes multi-host pass-through, deny, and logging checks. The report independently shows that defer is not neutral and that silent pass-through restores normal behavior. Preserve the Desktop reproduction and no-op isolation coverage alongside #2006.
fix(hooks): stop emitting permissionDecision defer from question-pref…
fix(hooks): tighten AUQ pass-through coverage and restore spike docs
fix(gstack2): packaged QUESTION-FORMAT contract + documented-only hoo…
v1.61.0.0 fix wave: guards failing open / silent failures (9 fixes, 4…