Skip to main content
The Claude Agent SDK provides flexible permission management through permission modes and tool permission callbacks. These features let you control which tools Claude can use and validate tool inputs before execution.

Permission Modes

Permission modes control how Claude Agent handles tool permission requests. There are four modes:
ModeDescriptionUse Case
defaultPrompt user for each tool useInteractive development, security-critical operations
acceptEditsAuto-approve file edits (Read, Write, Edit)Automated file manipulation workflows
planPlan mode with limited permissionsStrategic planning without execution
bypassPermissionsAuto-approve all toolsTesting, trusted environments

Setting Permission Mode

from claude_agent_sdk import ClaudeAgentOptions, query

options = ClaudeAgentOptions(
    permission_mode="acceptEdits",
    allowed_tools=["Read", "Write", "Edit"]
)

async for message in query(
    prompt="Create a hello.py file",
    options=options
):
    print(message)

Permission Mode Examples

# User is prompted for each tool use
options = ClaudeAgentOptions(
    permission_mode="default"
)
bypassPermissions mode auto-approves all tool usage. Only use this in trusted environments or for testing purposes.

Tool Permission Callbacks

For fine-grained control, use the can_use_tool callback to programmatically approve or deny tool requests based on custom logic.

Basic Permission Callback

from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    PermissionResultAllow,
    PermissionResultDeny,
    ToolPermissionContext
)

async def my_permission_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Allow read operations automatically
    if tool_name in ["Read", "Glob", "Grep"]:
        return PermissionResultAllow()
    
    # Deny dangerous operations
    if tool_name == "Bash" and "rm -rf" in input_data.get("command", ""):
        return PermissionResultDeny(
            message="Dangerous command detected"
        )
    
    # Allow by default
    return PermissionResultAllow()

options = ClaudeAgentOptions(
    can_use_tool=my_permission_callback,
    permission_mode="default"
)

async with ClaudeSDKClient(options=options) as client:
    await client.query("List files in current directory")
    async for msg in client.receive_response():
        print(msg)

Permission Callback with Input Modification

You can modify tool inputs before execution:
async def safe_path_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    if tool_name in ["Write", "Edit"]:
        file_path = input_data.get("file_path", "")
        
        # Block system directories
        if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
            return PermissionResultDeny(
                message=f"Cannot write to system directory: {file_path}"
            )
        
        # Redirect to safe directory
        if not file_path.startswith("./safe_output/"):
            modified_input = input_data.copy()
            modified_input["file_path"] = f"./safe_output/{file_path.split('/')[-1]}"
            return PermissionResultAllow(
                updated_input=modified_input
            )
    
    return PermissionResultAllow()

options = ClaudeAgentOptions(
    can_use_tool=safe_path_callback
)

Logging and Auditing

Track tool usage for compliance and debugging:
import json
from datetime import datetime

tool_audit_log = []

async def audit_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Log all tool requests
    audit_entry = {
        "timestamp": datetime.now().isoformat(),
        "tool": tool_name,
        "input": input_data,
        "suggestions": context.suggestions
    }
    tool_audit_log.append(audit_entry)
    
    print(f"[AUDIT] Tool: {tool_name}")
    print(f"[AUDIT] Input: {json.dumps(input_data, indent=2)}")
    
    # Apply permission logic
    if tool_name == "Bash":
        command = input_data.get("command", "")
        dangerous_patterns = ["rm -rf", "sudo", "chmod 777", "dd if="]
        
        for pattern in dangerous_patterns:
            if pattern in command:
                print(f"[AUDIT] DENIED: Dangerous pattern '{pattern}'")
                return PermissionResultDeny(
                    message=f"Dangerous command pattern detected: {pattern}"
                )
    
    print(f"[AUDIT] ALLOWED")
    return PermissionResultAllow()

options = ClaudeAgentOptions(
    can_use_tool=audit_callback
)

Permission Context

The ToolPermissionContext provides additional information:
from claude_agent_sdk import ToolPermissionContext

async def context_aware_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Access permission suggestions from CLI
    if context.suggestions:
        print(f"CLI suggestions: {context.suggestions}")
    
    # Future: abort signal support
    # if context.signal and context.signal.aborted:
    #     return PermissionResultDeny(message="Operation cancelled")
    
    return PermissionResultAllow()

Permission Updates

You can programmatically update permission settings:
from claude_agent_sdk import (
    PermissionUpdate,
    PermissionRuleValue,
    PermissionResultAllow
)

async def update_permissions_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Allow and update permissions for future use
    if tool_name == "Read":
        return PermissionResultAllow(
            updated_permissions=[
                PermissionUpdate(
                    type="addRules",
                    rules=[
                        PermissionRuleValue(
                            tool_name="Read",
                            rule_content="*.py"  # Allow reading Python files
                        )
                    ],
                    behavior="allow",
                    destination="session"  # Apply to current session only
                )
            ]
        )
    
    return PermissionResultAllow()
Permission update types:
  • addRules - Add new permission rules
  • replaceRules - Replace existing rules
  • removeRules - Remove specific rules
  • setMode - Change permission mode
  • addDirectories - Add trusted directories
  • removeDirectories - Remove trusted directories
Permission destinations:
  • session - Apply to current session only
  • userSettings - Save to user settings
  • projectSettings - Save to project settings
  • localSettings - Save to local settings

Combining Modes and Callbacks

You can use both permission modes and callbacks together:
async def selective_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Add custom logic on top of permission mode
    if tool_name == "WebFetch":
        url = input_data.get("url", "")
        # Block access to internal networks
        if url.startswith("http://localhost") or url.startswith("http://127.0.0.1"):
            return PermissionResultDeny(
                message="Cannot fetch from localhost"
            )
    
    return PermissionResultAllow()

options = ClaudeAgentOptions(
    permission_mode="acceptEdits",  # Auto-approve file edits
    can_use_tool=selective_callback  # Additional custom validation
)

Interrupting Tool Execution

You can interrupt tool execution from permission callbacks:
async def interrupt_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Deny with interrupt flag to stop execution immediately
    if tool_name == "Bash" and "critical" in input_data.get("command", ""):
        return PermissionResultDeny(
            message="Critical command blocked",
            interrupt=True  # Stop the agent immediately
        )
    
    return PermissionResultAllow()

Complete Example

Here’s a complete example combining multiple permission strategies:
import asyncio
import json
from claude_agent_sdk import (
    ClaudeAgentOptions,
    ClaudeSDKClient,
    PermissionResultAllow,
    PermissionResultDeny,
    ToolPermissionContext,
    AssistantMessage,
    ResultMessage,
    TextBlock
)

# Track tool usage
tool_usage_log = []

async def comprehensive_permission_callback(
    tool_name: str,
    input_data: dict,
    context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
    # Log the 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"   βœ… Auto-allowing {tool_name} (read-only)")
        return PermissionResultAllow()
    
    # Validate write operations
    if tool_name in ["Write", "Edit", "MultiEdit"]:
        file_path = input_data.get("file_path", "")
        
        # Block system directories
        if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
            print(f"   ❌ Denying write to system directory")
            return PermissionResultDeny(
                message=f"Cannot write to system directory: {file_path}"
            )
        
        # Redirect unsafe paths
        if not file_path.startswith("./"):
            safe_path = f"./safe_output/{file_path.split('/')[-1]}"
            print(f"   ⚠️  Redirecting to safe path: {safe_path}")
            modified_input = input_data.copy()
            modified_input["file_path"] = safe_path
            return PermissionResultAllow(updated_input=modified_input)
    
    # Validate bash commands
    if tool_name == "Bash":
        command = input_data.get("command", "")
        dangerous = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
        
        for pattern in dangerous:
            if pattern in command:
                print(f"   ❌ Denying dangerous command")
                return PermissionResultDeny(
                    message=f"Dangerous pattern detected: {pattern}",
                    interrupt=True
                )
        
        print(f"   βœ… Allowing bash command")
        return PermissionResultAllow()
    
    # Default: allow
    print(f"   βœ… Allowing {tool_name}")
    return PermissionResultAllow()

async def main():
    options = ClaudeAgentOptions(
        can_use_tool=comprehensive_permission_callback,
        permission_mode="default",
        cwd="."
    )
    
    async with ClaudeSDKClient(options=options) as client:
        await client.query(
            "Please list files in current directory and create a test.py file"
        )
        
        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(f"\nβœ… Complete - Duration: {message.duration_ms}ms")
    
    # Print usage summary
    print("\n" + "="*60)
    print("Tool Usage Summary")
    print("="*60)
    for i, usage in enumerate(tool_usage_log, 1):
        print(f"\n{i}. {usage['tool']}")
        print(f"   Input: {json.dumps(usage['input'], indent=6)}")

if __name__ == "__main__":
    asyncio.run(main())

Best Practices

Begin with permission_mode="default" and gradually relax permissions as you verify behavior. This prevents unexpected tool usage.
Permission callbacks give you full programmatic control. Use them when permission modes alone aren’t sufficient.
Always validate tool inputs in permission callbacks, especially for commands like Bash that can execute arbitrary code.
Maintain audit logs of permission decisions for compliance, debugging, and security analysis.
Always specify allowed_tools to limit which tools are available, even with permission callbacks.
Test your permission callbacks with various inputs to ensure they behave as expected in all scenarios.

Build docs developers (and LLMs) love