question-preference-hook emits invalid permissionDecision "defer" → aborts every AskUserQuestion call
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
# Bug: `question-preference-hook` emits `permissionDecision: "defer"` → aborts every AskUserQuestion call
**Repo:** garrytan/gstack · **Version:** v1.60.1.0 · **Host:** Claude Code (current)
## Summary
The PreToolUse hook `hosts/claude/hooks/question-preference-hook.ts` returns
`permissionDecision: "defer"` on its no-op path (the `defer()` helper). The
current Claude Code harness does not honor `"defer"` as a permission decision —
it treats the hook output as invalid and **aborts the tool call**. The user sees
`Tool execution was interrupted` / `[Tool result missing due to internal error]`.
Because `defer()` is the overwhelmingly common return path (every question that
isn't a marker-tagged, never-ask, two-way door with an unambiguous recommended
option), **AskUserQuestion fails on essentially every call.** The `deny()` path
is unaffected because `"deny"` is a valid value.
## Reproduction
1. On Claude Code with gstack hooks enabled, trigger any `AskUserQuestion` call
without a matching never-ask preference.
2. Hook returns `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer"}}`.
3. Tool aborts every time.
Direct hook check:
```
echo '{"tool_name":"AskUserQuestion","tool_input":{"questions":[{"question":"Q?","options":[{"label":"A"},{"label":"B"}]}]}}' \
| hosts/claude/hooks/question-preference-hook
# → {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"defer"}}
```
## Root cause
Valid `permissionDecision` values are `"allow" | "deny" | "ask"`. `"defer"` is
not honored. The canonical "no opinion, proceed with the normal permission flow"
signal is to **omit `permissionDecision` entirely** (exit 0, empty stdout, or
`hookSpecificOutput` carrying only `additionalContext`).
## Fix
In the `defer()` helper, stop emitting `permissionDecision: "defer"`. Emit empty
stdout + exit 0 when there's no `additionalContext`; otherwise emit
`hookSpecificOutput` with `hookEventName` + `additionalContext` only:
```ts
function defer(additionalContext?: string): void {
if (!additionalContext) {
process.exit(0);
}
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
additionalContext,
},
}),
);
process.exit(0);
}
```
This preserves the plan-tune Layer-8 memory-context feature (via
`additionalContext`) and the enforcement `deny()` path, while unbreaking every
non-enforced question. Verified: patched hook returns empty/exit 0 and
AskUserQuestion works end-to-end.
Contributor guide
Research direction
Start with hosts/claude/hooks/question-preference-hook.ts and inspect the defer() helper and its callers. Run the direct hook check from the issue, then verify that the no-op path produces no invalid permissionDecision while additionalContext is preserved and the AskUserQuestion call completes normally.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100