Skip to content

fix(hooks): don't emit invalid permissionDecision 'defer' — it swallows the AskUserQuestion widget#2325

Open
jawadakram20 wants to merge 2 commits into
garrytan:mainfrom
jawadakram20:fix/auq-defer-swallows-widget
Open

fix(hooks): don't emit invalid permissionDecision 'defer' — it swallows the AskUserQuestion widget#2325
jawadakram20 wants to merge 2 commits into
garrytan:mainfrom
jawadakram20:fix/auq-defer-swallows-widget

Conversation

@jawadakram20

@jawadakram20 jawadakram20 commented Jul 22, 2026

Copy link
Copy Markdown

The bug

question-preference-hook returns {"permissionDecision":"defer"} on its no-opinion path, intending "let the permission flow continue."

defer is not in Claude Code's PreToolUse schema, which accepts only allow / deny / ask. As of Claude Code 2.1.14 the unrecognized value is interpreted as defer the tool call: the call is dropped and logged as a hook_deferred_tool attachment. It never executes.

For AskUserQuestion this means the question widget never renders. The user sees nothing at all, and the agent is left waiting on an answer that can never arrive.

Captured in a real transcript:

tool_use          AskUserQuestion  toolu_01PWVYTunnejtJTZv3SF6Bsw
hook_success      PreToolUse:AskUserQuestion
                  stdout: {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer"}}
hook_deferred_tool  AskUserQuestion   <-- call swallowed here
user              "questions are not loading"

Blast radius

This is not limited to users with plan-tune preferences configured. defer() is the default path for every non-auto-decidable question:

  • no preference set
  • recommendation marker missing
  • always-ask preference
  • one-way-door safety override
  • empty/unparseable stdin (crash safety)
  • non-AUQ tool names (defensive)

So AskUserQuestion is broken for essentially every user on Claude Code 2.1.14 with gstack installed.

The fix

Omit permissionDecision entirely in defer(). That is the correct "no opinion" signal — normal permission flow continues, and additionalContext (the memory-cache injection) is still delivered.

The deny() paths are untouched: never-ask auto-decide and the Conductor prose redirect still enforce exactly as before.

Docs

docs/spikes/claude-code-hook-mutation.md documented "defer" — let permission flow continue as a valid value. That stale assumption is how this got written, so it's corrected here to prevent reintroduction.

Tests

Defer-path assertions changed from toBe('defer') to toBeUndefined(), matching the corrected contract.

bun test test/question-preference-hook.test.ts \
         test/memory-cache-injection.test.ts \
         test/gstack-question-log.test.ts

 51 pass
 0 fail
 101 expect() calls

Verified end-to-end on Claude Code 2.1.14: with the patch applied, AskUserQuestion renders and returns answers normally. Both PostToolUse AUQ hooks (question-log-hook, auq-error-fallback-hook) were also checked and are unaffected.

@trunk-io

trunk-io Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@jawadakram20

Copy link
Copy Markdown
Author

/trunk merge

@trunk-io

trunk-io Bot commented Jul 22, 2026

Copy link
Copy Markdown

An error occurred while submitting your PR to the queue: Only users that are a part of this repo's Trunk organization or have write permissions to the repo can submit a PR to the queue

…ws AskUserQuestion

The PreToolUse question-preference-hook returned
`{"permissionDecision":"defer"}` to mean "no opinion, let the permission
flow continue". That value is not in Claude Code's PreToolUse schema, which
accepts only `allow` / `deny` / `ask`.

As of Claude Code 2.1.14 the unrecognized value is interpreted as "defer the
tool call": the call is dropped and recorded as a `hook_deferred_tool`
attachment. For AskUserQuestion this means the question widget never renders
— the user sees nothing at all and the agent is left waiting. Observed in a
real transcript: hook_success with `"permissionDecision":"defer"`, then
`hook_deferred_tool`, then the user typing "questions are not loading".

Because the defer path is the default for every non-auto-decidable question
(no preference set, marker missing, always-ask, one-way door safety
override), this broke AskUserQuestion for essentially all users, not just
those with plan-tune preferences configured.

Fix: omit `permissionDecision` entirely in defer(). That is the correct
"no opinion" signal — normal permission flow continues and
`additionalContext` (the memory-cache injection) is still delivered.

The deny paths (never-ask auto-decide, Conductor prose redirect) are
untouched and still enforce.

Tests updated to assert `permissionDecision` is undefined on the defer path
rather than the string 'defer'. 51 pass / 0 fail across
question-preference-hook, memory-cache-injection and gstack-question-log.
@jawadakram20
jawadakram20 force-pushed the fix/auq-defer-swallows-widget branch from ade59ab to 2faf6ae Compare July 22, 2026 18:49
@jawadakram20

Copy link
Copy Markdown
Author

Heads up for maintainers: I'm an outside contributor pushing from a fork, so I can't submit this to the Trunk queue myself — my /trunk merge above was rejected with Only users that are a part of this repo's Trunk organization or have write permissions to the repo can submit a PR to the queue. (The bot prompt is posted automatically on fork PRs too, hence the confusion.)

CI also hasn't run yet — workflows from first-time fork contributors need the "Approve and run" click, so the combined status is still pending with 0 reported checks.

Whenever someone with write access has a moment, this needs an approve-and-run and then an enqueue from your side. Happy to rebase or adjust anything in the meantime.

For convenience, the test run locally on this branch:

bun test test/question-preference-hook.test.ts \
         test/memory-cache-injection.test.ts \
         test/gstack-question-log.test.ts

 51 pass
 0 fail
 101 expect() calls

Follow-up to the same file's corrected values list. The multi-hook precedence
line still presented 'defer' as a decision value, contradicting the note above
it. Precedence is deny > ask > allow; a hook with no opinion omits
permissionDecision entirely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant