Skip to content

fix(plan-tune): stop emitting invalid permissionDecision "defer" (breaks Conductor AskUserQuestion)#1816

Open
ztownsend wants to merge 1 commit into
garrytan:mainfrom
ztownsend:fix/auq-defer-permissiondecision
Open

fix(plan-tune): stop emitting invalid permissionDecision "defer" (breaks Conductor AskUserQuestion)#1816
ztownsend wants to merge 1 commit into
garrytan:mainfrom
ztownsend:fix/auq-defer-permissiondecision

Conversation

@ztownsend

Copy link
Copy Markdown

Problem

The plan-tune PreToolUse hook (hosts/claude/hooks/question-preference-hook.ts) emits permissionDecision: "defer" on its pass-through path (defer()), which fires on every ordinary AskUserQuestion that has no never-ask enforcement.

"defer" is not a valid Claude Code permissionDecision — the spec defines only allow / 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__AskUserQuestion bridge does not ignore it. An unrecognized permissionDecision on its own injected tool hangs the round-trip: the question never renders and no tool_result is returned, so the harness substitutes [Tool result missing due to internal error]. Net effect — with the gstack plan-tune hooks installed, AskUserQuestion is completely broken in Conductor (e.g. /plan-ceo-review can never ask a question).

How it was diagnosed

Empirically isolated by A/B: removing only this PreToolUse hook restores AUQ in Conductor; restoring it breaks it again. The PostToolUse log hook is innocent (it only fires after a successful AUQ).

Fix

Express "no opinion" the spec-correct way — emit no permissionDecision:

  • No additionalContext → emit nothing at all (empty stdout + exit 0, the canonical "no decision" response).
  • Have Layer 8 memory context → surface additionalContext alone, still with no permissionDecision.

The deny enforcement path is unchanged (deny is spec-valid).

Changes

  • hosts/claude/hooks/question-preference-hook.tsdefer() no longer emits permissionDecision.
  • test/question-preference-hook.test.ts — defer contract updated (defer ⇒ no permissionDecision); 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; additionalContext injection still asserted.
  • docs/spikes/claude-code-hook-mutation.md — corrected; it had incorrectly documented "defer" as a valid permissionDecision.

Testing

bun test test/question-preference-hook.test.ts test/memory-cache-injection.test.ts   # 22 pass
bun test test/hook-scripts.test.ts test/gstack-settings-hook-schema-aware.test.ts test/gstack-question-log.test.ts   # 66 pass

Follow-up (not in this PR)

The deny enforcement path (never-ask auto-decide) emits a spec-valid deny against mcp__conductor__AskUserQuestion, but whether Conductor's bridge handles a deny verdict on its own AUQ tool cleanly is untested. Worth a separate verification.

🤖 Generated with Claude Code

…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>
@jbetala7

jbetala7 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Verdict: looks correct — reproduced the bug on current main (3bef43bc) and validated the fix on this branch.

Bug confirmed on main. hosts/claude/hooks/question-preference-hook.ts defer() (L94-97) emits permissionDecision: 'defer', while the enforcement path (L109-110) correctly uses 'deny'. The Claude Code hook spec defines only allow / deny / ask, so native CC silently drops the unknown value and falls through — which is exactly why it went unnoticed. Your Conductor diagnosis is the missing piece: an unrecognized permissionDecision on the injected mcp__conductor__AskUserQuestion tool hangs the round-trip, and since defer() fires on every ordinary question with no never-ask rule, it takes out AUQ entirely under Conductor.

Fix validated. Checked out the PR branch and ran the targeted suite:

bun test test/question-preference-hook.test.ts
16 pass / 0 fail

The new contract is right: no additionalContext → empty stdout + exit 0 (canonical "no decision"); Layer 8 context → additionalContext alone, still no permissionDecision. The deny enforcement path is untouched, so the profile-poisoning / never-ask guarantees are preserved. Minimal and spec-correct.

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.

2 participants