Skip to content

AskUserQuestion PreToolUse hook's defer() hits claude-code#64389 race — fix: use allow instead #2292

Description

@lotofiyan

Bug

hosts/claude/hooks/question-preference-hook.ts's pass-through path (the common case: a plain AskUserQuestion call with no <gstack-qid:...> marker, or one that doesn't match a never-ask preference) resolves via:

function defer(additionalContext?: string): void {
  const out = { hookEventName: 'PreToolUse', permissionDecision: 'defer', ... };
  process.stdout.write(JSON.stringify({ hookSpecificOutput: out }));
  process.exit(0);
}

This hits a confirmed Claude Code SDK race: anthropics/claude-code#64389 — a PreToolUse hook resolving permissionDecision: "defer" can lose a race in the SDK and get fed a fabricated is_error: true, "[Tool result missing due to internal error]" tool_result instead of suspending correctly. A commenter on that thread confirms it reproduces against AskUserQuestion specifically, not just Bash.

Since this hook matches every AskUserQuestion / mcp__*__AskUserQuestion call, and the pass-through defer() branch is what fires for any plain question without gstack's internal marker (i.e. most real usage, any time a skill or the base agent asks a free-form question), this made AskUserQuestion unreliable-to-broken in the Claude Code VS Code extension host in my testing — deterministically failing across repeated calls in one session.

Root cause is upstream, but the workaround is local

The claude-code#64389 thread has two purported fixes:

  1. Delay the defer() write until after the assistant message stream closes (~200ms, reported as "deterministic across dozens of runs" by one reporter).
  2. Use allow/deny/ask instead of defer — the thread states plainly these are not affected by the race.

I tried (1) first; it did not reproduce reliably in my environment — still failed after adding the delay. Switched to (2): since AskUserQuestion has no real "normal permission system" step for defer to fall through to (unlike a gated Bash command), swapping the pass-through case to permissionDecision: "allow" is behaviorally equivalent here, and it resolved the issue — confirmed working across repeated live calls afterward.

Suggested fix

In defer() in question-preference-hook.ts (and check auq-error-fallback-hook.ts / any other PreToolUse hook using defer for AskUserQuestion for the same pattern), change:

permissionDecision: 'defer',

to

permissionDecision: 'allow',

for the pass-through case specifically (the enforcement deny() path is unaffected and doesn't need to change). Happy to open a PR with this if useful — for now filing as an issue since I wasn't sure whether you'd want this landed as-is or handled differently upstream (e.g. if defer is relied on elsewhere for permission-mode interop I haven't considered).

Environment

  • CLAUDE_CODE_ENTRYPOINT=claude-vscode, Claude Code VS Code extension, agent SDK 0.3.207, macOS.

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