fix(plan-tune): stop emitting invalid permissionDecision "defer" (breaks Conductor AskUserQuestion)#1816
Conversation
…aks Conductor AUQ) The PreToolUse question-preference-hook emitted `permissionDecision: "defer"` on its pass-through path. `"defer"` is not a valid Claude Code permissionDecision — the spec defines only `allow` / `deny` / `ask`. Native Claude Code silently ignored the unknown value and fell through to normal flow, so it appeared to work. Conductor's `mcp__conductor__AskUserQuestion` bridge does NOT ignore it: an unrecognized permissionDecision on its own injected tool hangs the round-trip, so the question never renders and no tool_result is returned (the harness substitutes "[Tool result missing due to internal error]"). Because defer() fires on every ordinary question with no never-ask enforcement, this broke AskUserQuestion entirely for Conductor users whenever the gstack plan-tune hooks were installed. Fix: express "no opinion" the spec-correct way — emit no permissionDecision. Emit nothing at all when there is no additionalContext; surface additionalContext (Layer 8 memory) alone otherwise. The deny enforcement path is unchanged (deny is spec-valid). - Update the defer() contract + tests (defer => no permissionDecision). - Add a Conductor regression test: ordinary AUQ question => empty stdout. - Correct docs/spikes/claude-code-hook-mutation.md, which incorrectly documented "defer" as a valid permissionDecision value. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Verdict: looks correct — reproduced the bug on current Bug confirmed on Fix validated. Checked out the PR branch and ran the targeted suite: The new contract is right: no |
Problem
The plan-tune
PreToolUsehook (hosts/claude/hooks/question-preference-hook.ts) emitspermissionDecision: "defer"on its pass-through path (defer()), which fires on every ordinaryAskUserQuestionthat has nonever-askenforcement."defer"is not a valid Claude CodepermissionDecision— the spec defines onlyallow/deny/ask. Native Claude Code silently ignores the unknown value and falls through to normal flow, so it appeared to work.Conductor's
mcp__conductor__AskUserQuestionbridge does not ignore it. An unrecognizedpermissionDecisionon its own injected tool hangs the round-trip: the question never renders and notool_resultis returned, so the harness substitutes[Tool result missing due to internal error]. Net effect — with the gstack plan-tune hooks installed,AskUserQuestionis completely broken in Conductor (e.g./plan-ceo-reviewcan never ask a question).How it was diagnosed
Empirically isolated by A/B: removing only this
PreToolUsehook restores AUQ in Conductor; restoring it breaks it again. ThePostToolUselog hook is innocent (it only fires after a successful AUQ).Fix
Express "no opinion" the spec-correct way — emit no
permissionDecision:additionalContext→ emit nothing at all (empty stdout + exit 0, the canonical "no decision" response).additionalContextalone, still with nopermissionDecision.The
denyenforcement path is unchanged (denyis spec-valid).Changes
hosts/claude/hooks/question-preference-hook.ts—defer()no longer emitspermissionDecision.test/question-preference-hook.test.ts— defer contract updated (defer ⇒ nopermissionDecision); new Conductor regression test asserting an ordinary AUQ question produces empty stdout.test/memory-cache-injection.test.ts,test/skill-e2e-plan-tune-cathedral.test.ts— defer assertions updated;additionalContextinjection still asserted.docs/spikes/claude-code-hook-mutation.md— corrected; it had incorrectly documented"defer"as a validpermissionDecision.Testing
Follow-up (not in this PR)
The
denyenforcement path (never-askauto-decide) emits a spec-validdenyagainstmcp__conductor__AskUserQuestion, but whether Conductor's bridge handles adenyverdict on its own AUQ tool cleanly is untested. Worth a separate verification.🤖 Generated with Claude Code