Tool permission callbacks allow you to control which tools Claude can use and modify their inputs before execution. This is essential for building secure applications.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/anthropics/claude-agent-sdk-python/llms.txt
Use this file to discover all available pages before exploring further.
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