Python SDK for Claude Code enabling developers to build AI-powered applications and agents with support for custom tools, hooks, and bidirectional interactive conversations
Control tool execution through permission modes and custom callbacks.
Claude Agent SDK offers four permission modes:
| Mode | Behavior | Use Case |
|---|---|---|
default | Prompts for dangerous tools | Interactive sessions with human oversight |
acceptEdits | Auto-approve file edits | Automation with file operations |
plan | Planning mode | Design/architecture phase |
bypassPermissions | Allow all tools | Controlled environments only ⚠️ |
from claude_agent_sdk import ClaudeAgentOptions
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit"],
permission_mode="acceptEdits" # No prompts for file operations
)options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
permission_mode="default" # Will prompt for dangerous operations
)options = ClaudeAgentOptions(
permission_mode="bypassPermissions" # Allow everything - use carefully!
)For fine-grained control, implement a custom permission callback:
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny
)
async def permission_callback(tool_name, tool_input, context):
# Allow Read operations
if tool_name == "Read":
return PermissionResultAllow(behavior="allow")
# Block Bash commands
if tool_name == "Bash":
return PermissionResultDeny(
behavior="deny",
message="Bash commands not allowed in this session"
)
# Allow everything else
return PermissionResultAllow(behavior="allow")
options = ClaudeAgentOptions(
can_use_tool=permission_callback,
allowed_tools=["Read", "Write", "Bash"]
)async def validate_bash_commands(tool_name, tool_input, context):
if tool_name != "Bash":
return PermissionResultAllow(behavior="allow")
command = tool_input.get("command", "")
# Block dangerous patterns
dangerous_patterns = ["rm -rf", "mkfs", "dd if=", "> /dev/"]
for pattern in dangerous_patterns:
if pattern in command:
return PermissionResultDeny(
behavior="deny",
message=f"Blocked dangerous command containing: {pattern}",
interrupt=True # Stop the entire conversation
)
return PermissionResultAllow(behavior="allow")ALLOWED_PATHS = ["/project/src", "/project/docs"]
async def path_permission_callback(tool_name, tool_input, context):
if tool_name in ["Read", "Write", "Edit"]:
file_path = tool_input.get("file_path", "")
# Check if path is in allowed directories
for allowed_path in ALLOWED_PATHS:
if file_path.startswith(allowed_path):
return PermissionResultAllow(behavior="allow")
return PermissionResultDeny(
behavior="deny",
message=f"File {file_path} is outside allowed directories"
)
return PermissionResultAllow(behavior="allow")async def interactive_permission_callback(tool_name, tool_input, context):
# Auto-allow safe operations
if tool_name in ["Read", "Grep", "Glob"]:
return PermissionResultAllow(behavior="allow")
# Ask for confirmation on modifications
if tool_name in ["Write", "Edit"]:
file_path = tool_input.get("file_path", "")
print(f"\nClaude wants to {tool_name}: {file_path}")
response = input("Allow? (y/n): ")
if response.lower() == 'y':
return PermissionResultAllow(behavior="allow")
else:
return PermissionResultDeny(
behavior="deny",
message="User denied permission"
)
return PermissionResultAllow(behavior="allow")You can modify tool inputs before execution:
async def modify_input_callback(tool_name, tool_input, context):
if tool_name == "Bash":
command = tool_input.get("command", "")
# Add safety flags to rm commands
if command.startswith("rm ") and "-i" not in command:
modified_command = command.replace("rm ", "rm -i ", 1)
return PermissionResultAllow(
behavior="allow",
updated_input={"command": modified_command}
)
return PermissionResultAllow(behavior="allow")Apply permission rules dynamically:
from claude_agent_sdk import PermissionUpdate, PermissionRuleValue
async def dynamic_permission_callback(tool_name, tool_input, context):
if tool_name == "Read":
# Allow and add rule for future reads
return PermissionResultAllow(
behavior="allow",
updated_permissions=[
PermissionUpdate(
type="addRules",
rules=[
PermissionRuleValue(
tool_name="Read",
rule_content="/project/**/*.py"
)
],
behavior="allow",
destination="session"
)
]
)
return PermissionResultAllow(behavior="allow")Change permissions mid-conversation:
import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
async with ClaudeSDKClient() as client:
# Phase 1: Review (default permissions)
await client.query("Review this code")
async for msg in client.receive_response():
print(msg)
# Phase 2: Implementation (accept edits)
await client.set_permission_mode("acceptEdits")
await client.query("Now fix the issues")
async for msg in client.receive_response():
print(msg)
anyio.run(main)import anyio
from pathlib import Path
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny
)
# Configuration
ALLOWED_PATHS = ["/workspace/src", "/workspace/docs"]
BLOCKED_COMMANDS = ["rm -rf", "format", "dd if=", "> /dev/", "mkfs"]
SAFE_TOOLS = ["Read", "Grep", "Glob"]
async def secure_permission_callback(tool_name, tool_input, context):
"""Secure permission callback for production environment."""
# Always allow safe read operations
if tool_name in SAFE_TOOLS:
return PermissionResultAllow(behavior="allow")
# Validate Bash commands
if tool_name == "Bash":
command = tool_input.get("command", "")
for pattern in BLOCKED_COMMANDS:
if pattern in command:
return PermissionResultDeny(
behavior="deny",
message=f"Security violation: command contains '{pattern}'",
interrupt=True
)
# Log all allowed commands
print(f"[AUDIT] Bash command: {command}")
return PermissionResultAllow(behavior="allow")
# Validate file operations
if tool_name in ["Write", "Edit"]:
file_path = tool_input.get("file_path", "")
# Check if in allowed paths
allowed = any(file_path.startswith(p) for p in ALLOWED_PATHS)
if not allowed:
return PermissionResultDeny(
behavior="deny",
message=f"File operations only allowed in: {', '.join(ALLOWED_PATHS)}"
)
# Block sensitive files
if any(s in file_path for s in [".env", "credentials", "secrets"]):
return PermissionResultDeny(
behavior="deny",
message="Cannot modify sensitive configuration files",
interrupt=True
)
print(f"[AUDIT] File operation: {tool_name} on {file_path}")
return PermissionResultAllow(behavior="allow")
# Deny unknown tools
return PermissionResultDeny(
behavior="deny",
message=f"Tool {tool_name} not in approved list"
)
async def main():
options = ClaudeAgentOptions(
can_use_tool=secure_permission_callback,
allowed_tools=["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
cwd="/workspace"
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Update the documentation and run tests")
async for msg in client.receive_response():
print(msg)
anyio.run(main)# Development: Allow edits
dev_options = ClaudeAgentOptions(permission_mode="acceptEdits")
# CI/CD: Bypass for automation
ci_options = ClaudeAgentOptions(permission_mode="bypassPermissions")
# Production: Use callbacks for fine control
prod_options = ClaudeAgentOptions(can_use_tool=strict_callback)# Good: Whitelist approach
ALLOWED_TOOLS = ["Read", "Grep", "Glob"]
async def whitelist_callback(tool_name, tool_input, context):
if tool_name in ALLOWED_TOOLS:
return PermissionResultAllow(behavior="allow")
return PermissionResultDeny(behavior="deny", message="Tool not allowed")
# Avoid: Blacklist approach (easy to miss dangerous tools)
BLOCKED_TOOLS = ["Bash"] # What about other dangerous tools?import logging
logger = logging.getLogger(__name__)
async def logging_callback(tool_name, tool_input, context):
logger.info(f"Permission request: {tool_name} with input {tool_input}")
# ... permission logic ...
if denied:
logger.warning(f"Permission denied: {tool_name}")async def fail_secure_callback(tool_name, tool_input, context):
try:
# Permission logic
return check_permission(tool_name, tool_input)
except Exception as e:
# On error, deny rather than allow
logger.error(f"Permission check error: {e}")
return PermissionResultDeny(
behavior="deny",
message="Permission check failed"
)options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob"],
permission_mode="default"
)options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit"],
permission_mode="acceptEdits"
)async def no_network_callback(tool_name, tool_input, context):
if tool_name in ["WebFetch", "WebSearch"]:
return PermissionResultDeny(behavior="deny", message="Network access disabled")
return PermissionResultAllow(behavior="allow")audit_log = []
async def audit_callback(tool_name, tool_input, context):
audit_log.append({
"tool": tool_name,
"input": tool_input,
"timestamp": datetime.now()
})
return PermissionResultAllow(behavior="allow")Install with Tessl CLI
npx tessl i tessl/pypi-claude-agent-sdk@0.1.3