Skip to content

AskUserQuestion silently broken: PreToolUse hook emits permissionDecision:'defer' → 'Tool result missing due to internal error' #2035

Closed

Description

@smorando01

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)
  • bun present 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

  1. gstack 1.55.1.0 with the PreToolUse hook registered in ~/.claude/settings.json (matcher: "(AskUserQuestion|mcp__.*__AskUserQuestion)").
  2. Trigger any AskUserQuestion that has no matching never-ask preference.
  3. 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

  1. hotsauce0817 commented on Jun 23, 2026

    @hotsauce0817

    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. Every AskUserQuestion call returns [Tool result missing due to internal error] and the picker never renders.

    Isolated to a single variable: with the PreToolUse question-preference-hook emitting permissionDecision:"defer" → broken; with it emitting nothing (exit 0) → works. So this isn't Conductor-specific — the mainstream Desktop host is also strict about the unknown defer value. Fixed by #1924 / #1816; flagging the broader blast radius as a reason to prioritize the merge.

  2. optimization2026 commented on Jun 30, 2026

    @optimization2026

    This looks like a tool permission decision contract issue.

    AskUserQuestion is 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:

    1. Treat AskUserQuestion as a control-flow tool.
    2. Do not let defer become an internal missing-result state.
    3. Add an explicit mapping table for permissionDecision values.
    4. Add regression test for AskUserQuestion + defer.
    

    The invariant:

    a user-question tool must never fail as an internal missing tool result when the intended state is "ask the user".
    
  3. time-attack commented on Jul 15, 2026

    @time-attack
    Contributor

    @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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions