Skip to content

Commit de41979

Browse files
committed
Add canUseTool permission support to Python SDK
This commit completes the canUseTool permission functionality implementation in the Python SDK to match the TypeScript SDK behavior: ## Key Changes: 1. **Transport Layer Enhancement** (`subprocess_cli.py`): - Added `--can-use-tool` CLI flag when canUseTool callback is provided - Ensures the CLI process knows to enable permission callback mode 2. **Example Implementation** (`can_use_tool_example.py`): - Comprehensive example showing how to use canUseTool callbacks - Demonstrates security policies, input modification, and denial patterns - Shows integration with ClaudeSDKClient in streaming mode ## Features Already Implemented: - **Type Definitions**: CanUseTool, PermissionResult*, ToolPermissionContext in `types.py` - **Client Integration**: Full canUseTool support in ClaudeSDKClient with proper validation - **Query Support**: Both query() function and ClaudeSDKClient handle canUseTool callbacks - **Control Protocol**: Complete bidirectional permission request/response handling - **Input Validation**: Ensures streaming mode is required, mutual exclusion with permission_prompt_tool_name - **Comprehensive Tests**: Full test coverage in test_tool_callbacks.py The implementation follows the TypeScript SDK's architecture exactly, providing: - Tool permission interception before execution - Input modification capabilities - Permission updates and suggestions - Proper error handling and validation πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 333491e commit de41979

2 files changed

Lines changed: 94 additions & 0 deletions

File tree

β€Žcan_use_tool_example.pyβ€Ž

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
"""Example demonstrating canUseTool permission functionality."""
3+
4+
import asyncio
5+
from claude_agent_sdk import (
6+
ClaudeAgentOptions,
7+
ClaudeSDKClient,
8+
CanUseTool,
9+
PermissionResultAllow,
10+
PermissionResultDeny,
11+
ToolPermissionContext,
12+
)
13+
14+
15+
async def security_callback(
16+
tool_name: str, input_data: dict, context: ToolPermissionContext
17+
) -> PermissionResultAllow | PermissionResultDeny:
18+
"""Example security callback that implements tool permission policy."""
19+
print(f"πŸ”’ Permission check for tool: {tool_name}")
20+
print(f" Input: {input_data}")
21+
22+
# Example policy: deny dangerous bash commands
23+
if tool_name == "Bash":
24+
command = input_data.get("command", "")
25+
dangerous_patterns = ["rm -rf", "dd if=", "mkfs", "format", "del *"]
26+
27+
if any(pattern in command for pattern in dangerous_patterns):
28+
print("❌ Denied: Command contains dangerous patterns")
29+
return PermissionResultDeny(
30+
message="Command blocked due to security policy",
31+
interrupt=True
32+
)
33+
34+
# Example: Allow but modify file operations to add safety
35+
if tool_name in ["Write", "Edit"]:
36+
file_path = input_data.get("file_path", "")
37+
if file_path.startswith("/etc/") or file_path.startswith("/system/"):
38+
print("⚠️ Modified: Adding backup for system file operation")
39+
modified_input = input_data.copy()
40+
modified_input["backup"] = True
41+
return PermissionResultAllow(updated_input=modified_input)
42+
43+
print("βœ… Allowed")
44+
return PermissionResultAllow()
45+
46+
47+
async def streaming_messages():
48+
"""Generate streaming messages for testing."""
49+
yield {
50+
"type": "user",
51+
"message": {"role": "user", "content": "What is 2+2?"},
52+
"parent_tool_use_id": None,
53+
"session_id": "test_session"
54+
}
55+
56+
57+
async def main():
58+
"""Main example function."""
59+
print("πŸš€ Testing canUseTool functionality")
60+
61+
try:
62+
# Create options with canUseTool callback
63+
options = ClaudeAgentOptions(
64+
can_use_tool=security_callback,
65+
permission_mode="default", # This will be overridden to use stdio
66+
)
67+
68+
print("πŸ“‹ Options created with canUseTool callback")
69+
70+
# Test with ClaudeSDKClient (streaming mode required for canUseTool)
71+
client = ClaudeSDKClient(options=options)
72+
73+
print("πŸ”Œ Connecting to Claude with canUseTool enabled...")
74+
await client.connect(streaming_messages())
75+
76+
print("βœ… Connection successful!")
77+
print("πŸ”’ canUseTool callback is now active and will intercept tool usage")
78+
79+
# Note: In a real scenario, you would now interact with the client
80+
# and the security_callback would be invoked for each tool use
81+
82+
await client.disconnect()
83+
print("βœ… Disconnected successfully")
84+
85+
except Exception as e:
86+
print(f"❌ Error: {e}")
87+
88+
89+
if __name__ == "__main__":
90+
asyncio.run(main())

β€Žsrc/claude_agent_sdk/_internal/transport/subprocess_cli.pyβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ def _build_command(self) -> list[str]:
181181
)
182182
cmd.extend(["--setting-sources", sources_value])
183183

184+
# Add canUseTool flag if callback is provided
185+
if self._options.can_use_tool:
186+
cmd.append("--can-use-tool")
187+
184188
# Add extra args for future CLI flags
185189
for flag, value in self._options.extra_args.items():
186190
if value is None:

0 commit comments

Comments
Β (0)