fix: always emit required fields in PermissionDecision wire format#50
Merged
guess merged 2 commits intoMay 22, 2026
Merged
Conversation
Bare `:allow` from a can_use_tool callback was producing
`{"behavior": "allow"}` — no `updatedInput` field. CLI 2.1.76's Zod
schema treats `updatedInput` as required on the "allow" arm of the
permission-response union, so every tool call routed through
can_use_tool fails validation with a ZodError and the agent stalls.
The same issue affected Deny — `{"behavior": "deny"}` with no `message`
fails the "deny" arm (which requires `message: string`).
Fix: default `updated_input` to %{} and `message` to "" in to_wire/1
so the wire payload always satisfies the CLI schema. Callers that
supply real values are unaffected.
Updated three tests that previously asserted the broken output.
Owner
|
@paperview thank you! LGTM. Can you just make sure the checks/tests pass and then i can merge in? 🙏 |
Contributor
Author
|
@guess sure thing, I believe this is now done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a
:can_use_toolcallback returns:allow(the documented shorthand for "allow this tool"), the resulting wire response is{"behavior": "allow"}— missing theupdatedInputfield that Claude CLI ≥ 2.1.76 requires. The CLI rejects the response withZodError, every tool call fails, and the agent has no way to make progress. The same issue affects{:deny, message: nil}and anyDenystruct without amessage.Verified on
claude_code0.36.3with bundled CLI2.1.76.Reproduction
Every tool call that goes through
can_use_toolresults in the CLI emitting an error like:Reading both branches: the CLI's permission-response schema is a discriminated union where the
"allow"arm requiresupdatedInput(a record) and the"deny"arm requiresmessage(a string). Our{"behavior": "allow"}fails the allow arm on missingupdatedInputand the deny arm on the literal mismatch. The agent then fabricates explanations ("I need permission to run this MCP tool", "settings.json must be misconfigured") and stalls.Root cause
lib/claude_code/hook/permission_decision/allow.ex:Output.maybe_put/3skips nil values. The:allowshorthand path is:…which leaves
updated_inputas nil, so it never lands in the wire payload. The Zod schema on the CLI side treatsupdatedInputas required.lib/claude_code/hook/permission_decision/deny.exhas the same shape — aDenystruct without amessagewould also produce{"behavior": "deny"}and fail validation.Expected behavior
:allowshould produce a wire payload that satisfies the CLI's permission-response schema unconditionally. Either:updatedInput(default to%{}when nil), orupdatedInputwhen the callback didn't provide an override.For
Deny, similarly: emitmessage(default to""or some neutral string) when the struct didn't carry one.Fix
Default
updated_inputto%{}andmessageto""in the twoto_wire/1functions, so the wire payload satisfies the CLI's Zod schema even when the callback doesn't supply those fields. Tests updated.A richer fix would thread the original tool input through
ControlHandler.handle_can_use_tool/3soAllow.to_wire/1can emit the actual input when the callback returns bare:allow. That avoids sending an empty map and matches how the Python SDK behaves — leaving that as a follow-up.Related
:allow/:denyin hook responses" — the typespec may have been corrected but the wire output wasn't.:can_use_toolwith CLI ≥ 2.1.76. With the bundled CLI version locked at 2.1.76 in 0.36.x this is universal.