claude-agent-sdk-python icon indicating copy to clipboard operation
claude-agent-sdk-python copied to clipboard

can_use_tool callback not getting called

Open hcoura opened this issue 4 months ago • 4 comments

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

hcoura avatar Sep 09 '25 16:09 hcoura

I spent all night on this, and came across 2 things:

  1. 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.
  2. 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!

EdanStarfire avatar Sep 15 '25 06:09 EdanStarfire

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:

  1. Updating the definition of the PermissionResultAllow and PermissionResultDeny to properly enable required fields to be required by the types (at least updated_input, message, and behavior). 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 the updated_permissions field or even how to test - I haven't gotten that far.
  2. 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!

EdanStarfire avatar Sep 15 '25 13:09 EdanStarfire

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...

hcoura avatar Sep 17 '25 19:09 hcoura

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

  1. No field duplication - only correct fields (behavior, updatedInput, message), no legacy fields
  2. Adds updatedPermissions serialization - enables "Always Allow" to persist permissions to .claude/settings.local.json
  3. 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

notabene00 avatar Oct 03 '25 09:10 notabene00