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, custom callbacks, and permission updates. Provides dynamic permission control during conversations, rule-based permissions, and permission result types.
Predefined permission behavior modes.
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]Modes:
"default": CLI prompts user for dangerous tools"acceptEdits": Auto-accept file edit operations (Read, Write, Edit, etc.)"plan": Plan mode for design/planning workflows"bypassPermissions": Allow all tools without prompting (use with extreme caution)Usage:
from claude_agent_sdk import query, ClaudeAgentOptions
# Auto-accept file edits
options = ClaudeAgentOptions(
permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit"]
)
async for message in query(
prompt="Fix the bugs in main.py",
options=options
):
print(message)Dynamic mode changes:
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async with ClaudeSDKClient() as client:
# Start in default mode
await client.query("Review this code")
async for msg in client.receive_response():
print(msg)
# Switch to auto-accept for implementation
await client.set_permission_mode("acceptEdits")
await client.query("Now implement the fixes")
async for msg in client.receive_response():
print(msg)Custom callback for fine-grained tool authorization. Requires streaming mode.
CanUseTool = Callable[
[str, dict[str, Any], ToolPermissionContext],
Awaitable[PermissionResult]
]Parameters:
Returns: PermissionResult (Allow or Deny)
Usage:
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
ToolPermissionContext,
PermissionResult,
PermissionResultAllow,
PermissionResultDeny
)
async def permission_callback(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext
) -> PermissionResult:
# Allow Read operations
if tool_name == "Read":
return PermissionResultAllow(behavior="allow")
# Check Bash commands for dangerous operations
if tool_name == "Bash":
command = tool_input.get("command", "")
# Block destructive commands
if any(pattern in command for pattern in ["rm -rf", "format", "dd if="]):
return PermissionResultDeny(
behavior="deny",
message=f"Dangerous command blocked: {command}",
interrupt=True # Stop the conversation
)
# Allow safe commands
return PermissionResultAllow(behavior="allow")
# Block Write to sensitive files
if tool_name in ["Write", "Edit"]:
file_path = tool_input.get("file_path", "")
if any(sensitive in file_path for sensitive in [".env", "credentials", "secrets"]):
return PermissionResultDeny(
behavior="deny",
message=f"Cannot modify sensitive file: {file_path}",
interrupt=False
)
# Default: allow
return PermissionResultAllow(behavior="allow")
options = ClaudeAgentOptions(
can_use_tool=permission_callback,
allowed_tools=["Read", "Write", "Bash"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Analyze and fix the codebase")
async for msg in client.receive_response():
print(msg)Context information for permission callbacks.
@dataclass
class ToolPermissionContext:
signal: Any | None = None
suggestions: list[PermissionUpdate] = field(default_factory=list)Fields:
signal: Future abort signal support (currently None)suggestions: Permission update suggestions from CLIUsage:
async def advanced_permission_callback(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext
) -> PermissionResult:
# Check CLI suggestions
if context.suggestions:
print(f"CLI suggests: {context.suggestions}")
# Permission logic
return PermissionResultAllow(behavior="allow")Permission granted with optional modifications.
@dataclass
class PermissionResultAllow:
behavior: Literal["allow"] = "allow"
updated_input: dict[str, Any] | None = None
updated_permissions: list[PermissionUpdate] | None = NoneFields:
behavior: Always "allow"updated_input: Modified tool input to use instead of originalupdated_permissions: Permission updates to applyUsage:
# Simple allow
result = PermissionResultAllow(behavior="allow")
# Allow with modified input
result = PermissionResultAllow(
behavior="allow",
updated_input={
"command": "ls -la /safe/path" # Modified command
}
)
# Allow with permission updates
from claude_agent_sdk import PermissionUpdate, PermissionRuleValue
result = PermissionResultAllow(
behavior="allow",
updated_permissions=[
PermissionUpdate(
type="addRules",
rules=[
PermissionRuleValue(
tool_name="Read",
rule_content="/project/**"
)
],
behavior="allow",
destination="session"
)
]
)Permission denied with optional message and interrupt.
@dataclass
class PermissionResultDeny:
behavior: Literal["deny"] = "deny"
message: str = ""
interrupt: bool = FalseFields:
behavior: Always "deny"message: Human-readable denial reasoninterrupt: Whether to stop the conversationUsage:
# Simple deny
result = PermissionResultDeny(behavior="deny")
# Deny with message
result = PermissionResultDeny(
behavior="deny",
message="This operation is not allowed in production"
)
# Deny and interrupt
result = PermissionResultDeny(
behavior="deny",
message="Critical security violation detected",
interrupt=True # Stops the conversation
)PermissionResult = PermissionResultAllow | PermissionResultDenyConfigure permission rule updates.
@dataclass
class PermissionUpdate:
type: Literal[
"addRules",
"replaceRules",
"removeRules",
"setMode",
"addDirectories",
"removeDirectories"
]
rules: list[PermissionRuleValue] | None = None
behavior: PermissionBehavior | None = None
mode: PermissionMode | None = None
directories: list[str] | None = None
destination: PermissionUpdateDestination | None = None
def to_dict(self) -> dict[str, Any]: ...Type Options:
PermissionBehavior = Literal["allow", "deny", "ask"]
PermissionUpdateDestination = Literal[
"userSettings",
"projectSettings",
"localSettings",
"session"
]Usage:
from claude_agent_sdk import PermissionUpdate, PermissionRuleValue
# Add rules
update = PermissionUpdate(
type="addRules",
rules=[
PermissionRuleValue(
tool_name="Read",
rule_content="/project/**/*.py"
)
],
behavior="allow",
destination="session"
)
# Set mode
update = PermissionUpdate(
type="setMode",
mode="acceptEdits",
destination="session"
)
# Add directories
update = PermissionUpdate(
type="addDirectories",
directories=["/path/to/include"],
destination="session"
)
# Convert to dict for CLI
update_dict = update.to_dict()Individual permission rule specification.
@dataclass
class PermissionRuleValue:
tool_name: str
rule_content: str | None = NoneFields:
tool_name: Tool name the rule applies torule_content: Rule content pattern (e.g., file path pattern)Usage:
from claude_agent_sdk import PermissionRuleValue
# Allow reading Python files
rule = PermissionRuleValue(
tool_name="Read",
rule_content="**/*.py"
)
# Allow all Bash commands
rule = PermissionRuleValue(
tool_name="Bash",
rule_content=None
)
# Specific file path
rule = PermissionRuleValue(
tool_name="Write",
rule_content="/project/output/**"
)import anyio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny
)
async def safe_permission_callback(tool_name, tool_input, context):
# Whitelist approach - only allow specific operations
safe_operations = {
"Read": True,
"Grep": True,
"Glob": True,
}
if tool_name in safe_operations:
return PermissionResultAllow(behavior="allow")
# Deny everything else
return PermissionResultDeny(
behavior="deny",
message=f"Tool {tool_name} not in safe operations whitelist"
)
async def main():
options = ClaudeAgentOptions(
can_use_tool=safe_permission_callback,
allowed_tools=["Read", "Write", "Bash", "Grep", "Glob"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Analyze the codebase but don't make any changes")
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
)
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 main():
options = ClaudeAgentOptions(
can_use_tool=path_permission_callback,
allowed_tools=["Read", "Write", "Edit"],
cwd="/project"
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Update the documentation files")
async for msg in client.receive_response():
print(msg)
anyio.run(main)import anyio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
PermissionResultAllow,
PermissionResultDeny
)
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")
# Require confirmation for 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"
)
# Require confirmation for Bash
if tool_name == "Bash":
command = tool_input.get("command", "")
print(f"\nClaude wants to run: {command}")
response = input("Allow? (y/n): ")
if response.lower() == 'y':
return PermissionResultAllow(behavior="allow")
else:
return PermissionResultDeny(
behavior="deny",
message="User denied command execution"
)
return PermissionResultAllow(behavior="allow")
async def main():
options = ClaudeAgentOptions(
can_use_tool=interactive_permission_callback,
allowed_tools=["Read", "Write", "Bash", "Grep"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Fix the bugs in the application")
async for msg in client.receive_response():
print(msg)
anyio.run(main)import anyio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
PermissionResultAllow
)
async def modify_input_callback(tool_name, tool_input, context):
if tool_name == "Bash":
command = tool_input.get("command", "")
# Add safety flags to commands
if command.startswith("rm ") and "-i" not in command:
# Add interactive flag to rm command
modified_command = command.replace("rm ", "rm -i ", 1)
return PermissionResultAllow(
behavior="allow",
updated_input={"command": modified_command}
)
return PermissionResultAllow(behavior="allow")
async def main():
options = ClaudeAgentOptions(
can_use_tool=modify_input_callback,
allowed_tools=["Bash"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Clean up temporary files")
async for msg in client.receive_response():
print(msg)
anyio.run(main)Install with Tessl CLI
npx tessl i tessl/pypi-claude-agent-sdk@0.1.3