can_use_tool callback not getting called
After attempting and failing to handle permission approval through can_use_tool callback I tried to run the example at: https://github.com/anthropics/claude-code-sdk-python/blob/839300404f89cf74a71abab2f9ad577d97cb8f1f/examples/tool_permission_callback.py and got the same behavior, the llm keeps looping until it gives up because permissions are never given.
My claude code version is 1.0.109
I spent all night on this, and came across 2 things:
- Getting the callback signature exactly right is important because it kinda gently suggests there's a problem with the callback and then continues trying. This can cause that looping behavior you are seeing. I'm my case, I was missing the context parameter because the website docs don't match this example, but the example you provided has the correct callback signature.
- Even with the callback signature correct, there is a bug in 0.0.22 that causes it to fall validation on both the PermissionResultAllow and PermissionResultDeny responses returned. This causes it to throw an error (again, relatively silently) and then moves on to loop. The example script actually hides the error completely when run, but it you print all the messages being received you'll see errors for missing fields from the response.
I'll write up a more formal response tomorrow detailing my first brush at getting it working tonight, but basically it returns a ZodError, which is company from the CC side, not the SDK. The current SDK passes an object like {"allow": True, "input": {...}}, but CC needs to be passed {"behavior": "allow", "updated input": {...}}. There's a similar one for the deny, but it requires {"behavior": "deny", "message":"..."}. These are handled in the internal/query.py file I believe. (I'm in bed right now so cannot check). If this is what you are running into, I'll just add the full details to this issue. Otherwise, I'll open a separate one. Thanks!
Details from my update above:
SDK Release: v0.0.22 Claude Version: v1.0.113
Steps to Reproduce: Update script in https://github.com/anthropics/claude-code-sdk-python/blob/main/examples/tool_permission_callback.py with the following on Line 132:
print(f"{message=}")
It'll throw an error similar to:
message=UserMessage(content=[ToolResultBlock(tool_use_id='toolu_014GSAq9rAFTYAmSzLaVTwYo', content='Tool permission request failed: [\n {\n "code":
"invalid_union",\n "unionErrors": [\n {\n "issues": [\n {\n "code": "invalid_literal",\n "expected":
"allow",\n "path": [\n "behavior"\n ],\n "message": "Invalid literal value, expected \\"allow\\""\n
},\n {\n "code": "invalid_type",\n "expected": "object",\n "received": "undefined",\n "path":
[\n "updatedInput"\n ],\n "message": "Required"\n }\n ],\n "name": "ZodError"\n },\n
{\n "issues": [\n {\n "code": "invalid_literal",\n "expected": "deny",\n "path": [\n
"behavior"\n ],\n "message": "Invalid literal value, expected \\"deny\\""\n },\n {\n "code":
"invalid_type",\n "expected": "string",\n "received": "undefined",\n "path": [\n "message"\n
],\n "message": "Required"\n }\n ],\n "name": "ZodError"\n }\n ],\n "path": [],\n "message": "Invalid
input"\n }\n]', is_error=True)]
This points at the behavior I found last night with being unable to apply permissions properly. The ZodError is coming from the CC implementation, not the python SDK.
Identified Fix
To fix, I've updated a couple things for testing purposes. This is clearly not the ideal fix, but it works and shows the issue. In _handle_control_request in http://github.com/anthropics/claude-code-sdk-python/blob/main/src/claude_code_sdk/_internal/query.py, I updated this section to add the required and misnamed fields:
# Convert PermissionResult to expected dict format
if isinstance(response, PermissionResultAllow):
response_data = {"allow": True, "behavior": "allow"}
if response.updated_input is not None:
response_data["input"] = response.updated_input
response_data["updatedInput"] = response.updated_input
# TODO: Handle updatedPermissions when control protocol supports it
elif isinstance(response, PermissionResultDeny):
response_data = {"allow": False, "behavior": "deny", "reason": response.message, "message": response.message}
# TODO: Handle interrupt flag when control protocol supports it
else:
raise TypeError(
f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}"
)
The proper fix I believe is:
- Updating the definition of the
PermissionResultAllowandPermissionResultDenyto properly enable required fields to be required by the types (at leastupdated_input,message, andbehavior). I believe these are the only required ones, but honestly haven't explored enough to identify if that's all. With those it's working, but there I'm unsure about theupdated_permissionsfield or even how to test - I haven't gotten that far. - Update the code that I tweaked above to ONLY include the proper fields. The test I did just ended up passing more stuff than needed, but hey, it was a test.
Not sure what else needs to be fixed for this, but the above make 0.0.22 work. Also, this is amazing to have this native python SDK!
Interesting, I did try to look on the repos to see what's the expected payload but couldn't find anywhere... let's see if some maintainer responds...
Complete Fix for All Three Permission Callback Bugs
I can confirm @EdanStarfire's findings about the behavior/updatedInput bug, and I've discovered two additional bugs that also prevent the permission callback from working properly.
Summary of All Three Bugs
Bug #1: Incorrect field names (already identified by @EdanStarfire)
- SDK sends: {"allow": true, "input": {...}}
- CLI expects: {"behavior": "allow", "updatedInput": {...}}
Bug #2: Missing updatedPermissions support
- SDK has # TODO: Handle updatedPermissions when control protocol supports it
- But CLI already supports it! Without this, "Always Allow" functionality doesn't work
Bug #3: Missing interrupt flag support
- SDK has # TODO: Handle interrupt flag when control protocol supports it
- But CLI already supports it! Without this, you can't distinguish between "deny and continue" vs "deny and interrupt"
Complete Fix
All three bugs are in claude_agent_sdk/_internal/query.py. Here's the complete corrected code (lines 219-241):
if isinstance(response, PermissionResultAllow):
response_data = {"behavior": "allow"}
if response.updated_input is not None:
response_data["updatedInput"] = response.updated_input
if response.updated_permissions is not None:
# Serialize PermissionUpdate to camelCase format for CLI
response_data["updatedPermissions"] = [
{
"type": perm.type,
"rules": [
{"toolName": rule.tool_name}
for rule in perm.rules
],
"behavior": perm.behavior,
"destination": perm.destination
}
for perm in response.updated_permissions
]
elif isinstance(response, PermissionResultDeny):
response_data = {"behavior": "deny", "message": response.message}
# CLI already supports interrupt flag
if response.interrupt:
response_data["interrupt"] = True
else:
raise TypeError(
f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}"
)
Key Differences from @EdanStarfire's Fix
- No field duplication - only correct fields (behavior, updatedInput, message), no legacy fields
- Adds updatedPermissions serialization - enables "Always Allow" to persist permissions to .claude/settings.local.json
- Adds interrupt flag - enables proper distinction between deny (continue processing) vs reject (interrupt execution)
Testing Results
Tested with Claude CLI v2.0.5 (confirmed CLI support for all fields).
After applying this fix, all permission callback behaviors work correctly:
- ✅ One-time Allow - PermissionResultAllow() works as expected
- ✅ Always Allow - PermissionResultAllow() with updated_permissions → successfully saves to .claude/settings.local.json
- ✅ Deny without interrupt - PermissionResultDeny(interrupt=False) → tool denied, processing continues
- ✅ Reject with interrupt - PermissionResultDeny(interrupt=True) → tool denied, execution stops
Why This Matters
The CLI was already ready to accept all these fields. The SDK just wasn't sending them due to TODO comments. This means:
- The control protocol already supports updatedPermissions and interrupt
- We just need to remove the TODO comments and properly serialize these fields
- No CLI changes needed - only SDK changes
Recommendation
These bugs make can_use_tool callback essentially non-functional. Without the fix:
- Permission responses fail validation (Zod error)
- "Always Allow" doesn't persist permissions
- Can't properly interrupt on rejection
This should be high priority since it blocks a core SDK feature.
Comment generated with assistance from Claude Code