mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 15:09:00 +02:00
fix(hooks): passThrough() two-branch contract — never emit permissionDecision:'defer' (#2035, #2006)
Every AskUserQuestion died with "Tool result missing due to internal error" on current Claude Code builds (Desktop 1.14271.0, CC 2.1.177). Root cause: the question-preference-hook emitted permissionDecision:'defer' on every pass-through path. 'defer' is a real PreToolUse value, but since CC v2.1.89 its semantics are "pause this tool call for external resumption" (headless resume) — never "abstain". Interactive sessions have nothing to resume the paused call, so the tool orphaned. Pre-2.1.89 builds ignored the unknown value, which is why the hook worked when it shipped and broke later. The fix is the two-branch pass-through contract: - no context -> exit 0 with EXACTLY empty stdout - memory nuggets present -> hookSpecificOutput with hookEventName + additionalContext ONLY (the documented shape; plan-tune Layer 8 memory injection ships through this branch and keeps working) defer() is renamed passThrough() so the function says what it does, and docs/spikes/claude-code-hook-mutation.md's protocol contract (cited by the hook header) is corrected in the same commit — it taught '"defer" — let permission flow continue' and was the reintroduction vector. Test contract rewritten in the same commit (13 assertions across 3 files, verified fail-first against the unfixed hook): pass-through paths assert exact-empty stdout (a garbage/partial write cannot slip past an optional-chained parse), the nugget path asserts permissionDecision is ABSENT while additionalContext survives, and a new tripwire asserts no non-deny path ever puts the string "permissionDecision" on stdout. The deny (auto-decide) and Conductor prose-redirect paths are unchanged. Deployment: no migration needed — settings.json points at the absolute bash shim which execs the .ts live; /gstack-upgrade delivers the fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c4efc2a4f7
commit
3d97863b14
@@ -51,7 +51,13 @@ Optional in subagent context: `agent_id`, `agent_type`.
|
||||
- `"deny"` — block (feedback to Claude, NOT a synthetic answer per Codex
|
||||
correction in D-prefixed decisions)
|
||||
- `"ask"` — escalate to user
|
||||
- `"defer"` — let permission flow continue
|
||||
- `"defer"` — pause the tool call for EXTERNAL resumption (Claude Code
|
||||
v2.1.89+, a headless feature: resume with `-p --resume` to re-evaluate).
|
||||
NEVER emit this to mean "no opinion" — in an interactive session nothing
|
||||
resumes the paused call and the tool dies with "Tool result missing due to
|
||||
internal error" (#2035, #2006). To abstain, exit 0 with EMPTY stdout
|
||||
(optionally `hookSpecificOutput` with `additionalContext` only, no
|
||||
`permissionDecision`).
|
||||
|
||||
**`updatedInput` semantics:** shallow merge of fields present in the returned
|
||||
object onto the original `tool_input`. Only valid with
|
||||
@@ -106,15 +112,20 @@ required for our hook to fire there.
|
||||
}
|
||||
```
|
||||
|
||||
**Pass-through (no preference, or one-way safety override):**
|
||||
**Pass-through (no preference, or one-way safety override):** exit 0 with
|
||||
EMPTY stdout. When there is context to inject (plan-tune memory nuggets),
|
||||
emit `additionalContext` WITHOUT a `permissionDecision`:
|
||||
```json
|
||||
{
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"permissionDecision": "defer"
|
||||
"additionalContext": "[plan-tune memory] Past answers suggest: ..."
|
||||
}
|
||||
}
|
||||
```
|
||||
(Historical note: this example originally emitted `permissionDecision:
|
||||
"defer"`, which broke every AskUserQuestion once CC v2.1.89 gave 'defer'
|
||||
pause-for-resume semantics — #2035.)
|
||||
|
||||
**PostToolUse capture (always):**
|
||||
```json
|
||||
|
||||
Reference in New Issue
Block a user