Basic Permission Callback
Create a callback function that returnsPermissionResultAllow or PermissionResultDeny:
from claude_agent_sdk import (
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
async def my_permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Control tool permissions based on tool type and input."""
# Always allow read operations
if tool_name in ["Read", "Glob", "Grep"]:
print(f"✅ Automatically allowing {tool_name} (read-only operation)")
return PermissionResultAllow()
# Deny other operations by default
return PermissionResultDeny(
message=f"Tool {tool_name} is not approved for use"
)
Allowing Specific Operations
Automatically approve safe, read-only operations:async def safe_permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Allow read operations, ask for write operations."""
print(f"\n🔧 Tool Permission Request: {tool_name}")
print(f" Input: {json.dumps(input_data, indent=2)}")
# Always allow read operations
if tool_name in ["Read", "Glob", "Grep"]:
print(f" ✅ Automatically allowing {tool_name} (read-only operation)")
return PermissionResultAllow()
# Prompt for write operations
user_input = input(f" Allow {tool_name}? (y/N): ").strip().lower()
if user_input in ("y", "yes"):
return PermissionResultAllow()
else:
return PermissionResultDeny(
message="User denied permission"
)
Denying Dangerous Operations
Block operations that could be harmful:import json
async def security_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Block dangerous operations."""
# Deny write operations to system directories
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = input_data.get("file_path", "")
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
print(f" ❌ Denying write to system directory: {file_path}")
return PermissionResultDeny(
message=f"Cannot write to system directory: {file_path}"
)
# Check dangerous bash commands
if tool_name == "Bash":
command = input_data.get("command", "")
dangerous_commands = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
for dangerous in dangerous_commands:
if dangerous in command:
print(f" ❌ Denying dangerous command: {command}")
return PermissionResultDeny(
message=f"Dangerous command pattern detected: {dangerous}"
)
print(f" ✅ Allowing bash command: {command}")
return PermissionResultAllow()
return PermissionResultAllow()
Modifying Tool Inputs
Redirect operations to safe locations by modifying inputs:async def redirect_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Redirect writes to a safe directory."""
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = input_data.get("file_path", "")
# Redirect writes to a safe directory
if not file_path.startswith("/tmp/") and not file_path.startswith("./"):
safe_path = f"./safe_output/{file_path.split('/')[-1]}"
print(f" ⚠️ Redirecting write from {file_path} to {safe_path}")
modified_input = input_data.copy()
modified_input["file_path"] = safe_path
return PermissionResultAllow(
updated_input=modified_input
)
return PermissionResultAllow()
Logging Tool Usage
Track which tools are being used:import json
# Track tool usage for demonstration
tool_usage_log = []
async def logging_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Log all tool requests."""
# Log the tool request
tool_usage_log.append({
"tool": tool_name,
"input": input_data,
"suggestions": context.suggestions
})
print(f"\n🔧 Tool Permission Request: {tool_name}")
print(f" Input: {json.dumps(input_data, indent=2)}")
# Allow all operations but log them
return PermissionResultAllow()
Using Permission Callbacks
Configure the callback inClaudeAgentOptions:
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock,
)
async def main():
# Configure options with our callback
options = ClaudeAgentOptions(
can_use_tool=my_permission_callback,
permission_mode="default", # Ensure callbacks are invoked
cwd="." # Set working directory
)
# Create client and send a query
async with ClaudeSDKClient(options) as client:
await client.query(
"Please do the following:\n"
"1. List the files in the current directory\n"
"2. Create a simple Python hello world script at hello.py\n"
"3. Run the script to test it"
)
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(f"\n💬 Claude: {block.text}")
elif isinstance(message, ResultMessage):
print("\n✅ Task completed!")
print(f" Duration: {message.duration_ms}ms")
if message.total_cost_usd:
print(f" Cost: ${message.total_cost_usd:.4f}")
if __name__ == "__main__":
asyncio.run(main())
Complete Example
Full working example with comprehensive callbacks
Full working example with comprehensive callbacks
#!/usr/bin/env python3
"""Example: Tool Permission Callbacks.
This example demonstrates how to use tool permission callbacks to control
which tools Claude can use and modify their inputs.
"""
import asyncio
import json
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
PermissionResultAllow,
PermissionResultDeny,
ResultMessage,
TextBlock,
ToolPermissionContext,
)
# Track tool usage for demonstration
tool_usage_log = []
async def my_permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Control tool permissions based on tool type and input."""
# Log the tool request
tool_usage_log.append({
"tool": tool_name,
"input": input_data,
"suggestions": context.suggestions
})
print(f"\n🔧 Tool Permission Request: {tool_name}")
print(f" Input: {json.dumps(input_data, indent=2)}")
# Always allow read operations
if tool_name in ["Read", "Glob", "Grep"]:
print(f" ✅ Automatically allowing {tool_name} (read-only operation)")
return PermissionResultAllow()
# Deny write operations to system directories
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = input_data.get("file_path", "")
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
print(f" ❌ Denying write to system directory: {file_path}")
return PermissionResultDeny(
message=f"Cannot write to system directory: {file_path}"
)
# Redirect writes to a safe directory
if not file_path.startswith("/tmp/") and not file_path.startswith("./"):
safe_path = f"./safe_output/{file_path.split('/')[-1]}"
print(f" ⚠️ Redirecting write from {file_path} to {safe_path}")
modified_input = input_data.copy()
modified_input["file_path"] = safe_path
return PermissionResultAllow(
updated_input=modified_input
)
# Check dangerous bash commands
if tool_name == "Bash":
command = input_data.get("command", "")
dangerous_commands = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
for dangerous in dangerous_commands:
if dangerous in command:
print(f" ❌ Denying dangerous command: {command}")
return PermissionResultDeny(
message=f"Dangerous command pattern detected: {dangerous}"
)
# Allow but log the command
print(f" ✅ Allowing bash command: {command}")
return PermissionResultAllow()
# For all other tools, ask the user
print(f" ❓ Unknown tool: {tool_name}")
print(f" Input: {json.dumps(input_data, indent=6)}")
user_input = input(" Allow this tool? (y/N): ").strip().lower()
if user_input in ("y", "yes"):
return PermissionResultAllow()
else:
return PermissionResultDeny(
message="User denied permission"
)
async def main():
"""Run example with tool permission callbacks."""
print("=" * 60)
print("Tool Permission Callback Example")
print("=" * 60)
print("\nThis example demonstrates how to:")
print("1. Allow/deny tools based on type")
print("2. Modify tool inputs for safety")
print("3. Log tool usage")
print("4. Prompt for unknown tools")
print("=" * 60)
# Configure options with our callback
options = ClaudeAgentOptions(
can_use_tool=my_permission_callback,
# Use default permission mode to ensure callbacks are invoked
permission_mode="default",
cwd="." # Set working directory
)
# Create client and send a query that will use multiple tools
async with ClaudeSDKClient(options) as client:
print("\n📝 Sending query to Claude...")
await client.query(
"Please do the following:\n"
"1. List the files in the current directory\n"
"2. Create a simple Python hello world script at hello.py\n"
"3. Run the script to test it"
)
print("\n📨 Receiving response...")
message_count = 0
async for message in client.receive_response():
message_count += 1
if isinstance(message, AssistantMessage):
# Print Claude's text responses
for block in message.content:
if isinstance(block, TextBlock):
print(f"\n💬 Claude: {block.text}")
elif isinstance(message, ResultMessage):
print("\n✅ Task completed!")
print(f" Duration: {message.duration_ms}ms")
if message.total_cost_usd:
print(f" Cost: ${message.total_cost_usd:.4f}")
print(f" Messages processed: {message_count}")
# Print tool usage summary
print("\n" + "=" * 60)
print("Tool Usage Summary")
print("=" * 60)
for i, usage in enumerate(tool_usage_log, 1):
print(f"\n{i}. Tool: {usage['tool']}")
print(f" Input: {json.dumps(usage['input'], indent=6)}")
if usage['suggestions']:
print(f" Suggestions: {usage['suggestions']}")
if __name__ == "__main__":
asyncio.run(main())
Permission Context
TheToolPermissionContext provides additional information:
class ToolPermissionContext:
suggestions: list[str] # Suggested actions or warnings
async def context_aware_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Use context to inform decisions."""
# Check if there are any warnings
if context.suggestions:
print(f"⚠️ Warnings: {context.suggestions}")
return PermissionResultAllow()
Key Takeaways
- Use
can_use_toolparameter inClaudeAgentOptionsto set your callback - Return
PermissionResultAllow()to allow tool use - Return
PermissionResultDeny(message="...")to deny tool use - Use
updated_inputto modify tool inputs before execution - Set
permission_mode="default"to ensure callbacks are invoked - Log tool usage for auditing and debugging
Next Steps
Hooks
Advanced customization with hooks
MCP Calculator
Build custom MCP tools