Python SDK for programmatically interacting with Claude Code, enabling AI-powered automation workflows with support for bidirectional conversations, custom in-process tools, hooks, and fine-grained permission control
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
—
The risk profile of this skill
This plugin was archived by the owner on Jun 3, 2026
Reason: Retiring all tiles created prior to the transition to plugin support
Runtime permission control via modes, programmatic callbacks, and permission updates. The permission system provides multiple layers of control over which tools Claude can execute and how.
Predefined permission modes for common scenarios.
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]Modes:
"default": CLI prompts user for dangerous tools"acceptEdits": Auto-accept file edits (Read, Write, Edit, MultiEdit)"plan": Plan mode, no actual execution"bypassPermissions": Allow all tools without prompts (use with caution)Usage Example:
from claude_agent_sdk import ClaudeAgentOptions
# Default mode - prompt for dangerous operations
options = ClaudeAgentOptions(
permission_mode="default",
allowed_tools=["Read", "Write", "Bash"]
)
# Accept edits automatically
options = ClaudeAgentOptions(
permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit"]
)
# Plan mode - no execution
options = ClaudeAgentOptions(
permission_mode="plan",
allowed_tools=["Read", "Write", "Bash"]
)
# Bypass all permissions (dangerous!)
options = ClaudeAgentOptions(
permission_mode="bypassPermissions",
allowed_tools=["Read", "Write", "Bash"]
)Change permission mode during conversation with ClaudeSDKClient.
Usage Example:
from claude_agent_sdk import ClaudeSDKClient
async def main():
async with ClaudeSDKClient() as client:
# Start with default permissions (prompts user)
await client.query("Review this codebase")
async for msg in client.receive_response():
print(msg)
# Switch to auto-accept edits
await client.set_permission_mode('acceptEdits')
await client.query("Now implement the fixes we discussed")
async for msg in client.receive_response():
print(msg)
# Back to default for safety
await client.set_permission_mode('default')Programmatic permission control via callback function.
CanUseTool = Callable[
[str, dict[str, Any], ToolPermissionContext],
Awaitable[PermissionResult]
]Parameters:
tool_name (str): Name of the tool being invokedtool_input (dict[str, Any]): Tool input parameterscontext (ToolPermissionContext): Permission context with signal and suggestionsReturns:
PermissionResult: Either PermissionResultAllow or PermissionResultDeny
Usage Example:
from claude_agent_sdk import (
ClaudeAgentOptions, PermissionResultAllow, PermissionResultDeny
)
async def my_permission_handler(tool_name, tool_input, context):
# Allow read operations
if tool_name == "Read":
return PermissionResultAllow()
# Review bash commands
if tool_name == "Bash":
command = tool_input.get("command", "")
if "rm -rf" in command:
return PermissionResultDeny(
message="Dangerous command blocked",
interrupt=True
)
return PermissionResultAllow()
# Allow other operations
return PermissionResultAllow()
options = ClaudeAgentOptions(
can_use_tool=my_permission_handler,
allowed_tools=["Read", "Write", "Bash"]
)Context provided to permission callbacks.
@dataclass
class ToolPermissionContext:
"""Context for permission callbacks."""
signal: Any | None = None
suggestions: list[PermissionUpdate] = field(default_factory=list)Fields:
signal (Any | None): Future abort signal support. Currently always None.
suggestions (list[PermissionUpdate]): Permission update suggestions from CLI based on the operation being performed.
Usage Example:
async def smart_permission_handler(tool_name, tool_input, context):
# Use CLI suggestions
suggestions = context.suggestions
# Apply suggested permission updates
if suggestions:
return PermissionResultAllow(updated_permissions=suggestions)
return PermissionResultAllow()Permission decisions returned from callbacks.
@dataclass
class PermissionResultAllow:
"""Allow permission decision."""
behavior: Literal["allow"] = "allow"
updated_input: dict[str, Any] | None = None
updated_permissions: list[PermissionUpdate] | None = NoneFields:
behavior (Literal["allow"]): Always "allow". Default value.
updated_input (dict[str, Any] | None): Modified tool input parameters. Use this to sanitize or enhance tool inputs before execution.
updated_permissions (list[PermissionUpdate] | None): Permission updates to apply. See Permission Updates below.
Usage Examples:
# Simple allow
return PermissionResultAllow()
# Allow with modified input
modified_input = tool_input.copy()
modified_input["timeout"] = 30
return PermissionResultAllow(updated_input=modified_input)
# Allow with permission updates
updates = [PermissionUpdate(
type="addRules",
rules=[PermissionRuleValue(tool_name="Bash", rule_content=None)],
behavior="allow",
destination="session"
)]
return PermissionResultAllow(updated_permissions=updates)
# Allow with both
return PermissionResultAllow(
updated_input=modified_input,
updated_permissions=updates
)@dataclass
class PermissionResultDeny:
"""Deny permission decision."""
behavior: Literal["deny"] = "deny"
message: str = ""
interrupt: bool = FalseFields:
behavior (Literal["deny"]): Always "deny". Default value.
message (str): Denial message shown to user and Claude. Default: empty string.
interrupt (bool): Whether to interrupt the entire agent execution. Default: False.
Usage Examples:
# Simple deny
return PermissionResultDeny()
# Deny with message
return PermissionResultDeny(
message="This operation is not allowed in production"
)
# Deny and interrupt
return PermissionResultDeny(
message="Critical security violation detected",
interrupt=True
)PermissionResult = PermissionResultAllow | PermissionResultDenyConfigure permission changes to apply.
@dataclass
class PermissionUpdate:
"""Permission configuration update."""
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]:
"""Convert to dictionary for CLI."""Fields:
type: Update type determining which other fields are required:
"addRules": Add permission rules (requires rules, behavior)"replaceRules": Replace permission rules (requires rules, behavior)"removeRules": Remove permission rules (requires rules)"setMode": Set permission mode (requires mode)"addDirectories": Add permission directories (requires directories)"removeDirectories": Remove permission directories (requires directories)rules (list[PermissionRuleValue] | None): Permission rules for add/replace/remove operations.
behavior (PermissionBehavior | None): Rule behavior ("allow", "deny", "ask").
mode (PermissionMode | None): Permission mode for setMode operation.
directories (list[str] | None): Directory list for add/remove operations.
destination (PermissionUpdateDestination | None): Where to apply update:
"userSettings": User-level settings"projectSettings": Project-level settings"localSettings": Local directory settings"session": Current session onlyMethods:
to_dict() -> dict[str, Any]: Convert to dictionary format for CLI protocol.Usage Examples:
from claude_agent_sdk import PermissionUpdate, PermissionRuleValue
# Add allow rule for session
update = PermissionUpdate(
type="addRules",
rules=[PermissionRuleValue(tool_name="Bash", rule_content=None)],
behavior="allow",
destination="session"
)
# Set permission mode
update = PermissionUpdate(
type="setMode",
mode="acceptEdits",
destination="session"
)
# Add directories
update = PermissionUpdate(
type="addDirectories",
directories=["/home/user/project/src"],
destination="projectSettings"
)
# Remove rules
update = PermissionUpdate(
type="removeRules",
rules=[PermissionRuleValue(tool_name="Bash", rule_content="rm -rf")],
destination="session"
)
# Convert to dict for protocol
update_dict = update.to_dict()Individual permission rule.
@dataclass
class PermissionRuleValue:
"""Permission rule value."""
tool_name: str
rule_content: str | None = NoneFields:
tool_name (str): Tool name pattern (e.g., "Bash", "Write", "*" for all).
rule_content (str | None): Optional rule content for fine-grained control (e.g., path patterns, command patterns).
Usage Example:
from claude_agent_sdk import PermissionRuleValue
# Simple tool rule
rule = PermissionRuleValue(tool_name="Bash")
# Rule with content pattern
rule = PermissionRuleValue(
tool_name="Write",
rule_content="/tmp/*" # Allow writes only to /tmp
)
# Wildcard rule
rule = PermissionRuleValue(tool_name="*") # All toolsRule behavior types.
PermissionBehavior = Literal["allow", "deny", "ask"]Values:
"allow": Allow the operation"deny": Deny the operation"ask": Prompt the userWhere permission updates are applied.
PermissionUpdateDestination = Literal[
"userSettings",
"projectSettings",
"localSettings",
"session"
]Values:
"userSettings": User-level settings (persists across all projects)"projectSettings": Project-level settings (persists for this project)"localSettings": Local directory settings (persists for this directory)"session": Current session only (does not persist)from claude_agent_sdk import (
ClaudeAgentOptions, ClaudeSDKClient,
PermissionResultAllow, PermissionResultDeny,
PermissionUpdate, PermissionRuleValue
)
import re
DANGEROUS_COMMANDS = [
r"rm\s+-rf\s+/",
r"mkfs\.",
r"dd\s+if=.*\s+of=/dev/",
r":\(\)\{\s*:\|:\&\s*\};:", # Fork bomb
]
async def security_permission_handler(tool_name, tool_input, context):
# Always allow read operations
if tool_name == "Read":
return PermissionResultAllow()
# Review bash commands
if tool_name == "Bash":
command = tool_input.get("command", "")
# Check for dangerous patterns
for pattern in DANGEROUS_COMMANDS:
if re.search(pattern, command):
return PermissionResultDeny(
message=f"Blocked dangerous command pattern: {pattern}",
interrupt=True
)
# Add timeout to long-running commands
if "timeout" not in tool_input:
modified_input = tool_input.copy()
modified_input["timeout"] = 60
return PermissionResultAllow(updated_input=modified_input)
# Review file writes
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = tool_input.get("file_path", "")
# Block writes to system directories
if file_path.startswith("/etc/") or file_path.startswith("/sys/"):
return PermissionResultDeny(
message=f"Cannot write to system directory: {file_path}"
)
# Allow with default behavior
return PermissionResultAllow()
# Use with client
async def main():
options = ClaudeAgentOptions(
can_use_tool=security_permission_handler,
allowed_tools=["Read", "Write", "Bash"]
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Review and fix the code")
async for msg in client.receive_response():
print(msg)from claude_agent_sdk import (
PermissionResultAllow, PermissionResultDeny,
PermissionUpdate, PermissionRuleValue
)
class AdaptivePermissionHandler:
def __init__(self):
self.trust_level = 0
self.operations_count = 0
async def __call__(self, tool_name, tool_input, context):
self.operations_count += 1
# Build trust over successful operations
if self.operations_count > 10:
self.trust_level = min(self.trust_level + 1, 5)
# High trust - allow most operations
if self.trust_level >= 4:
return PermissionResultAllow()
# Medium trust - review risky operations
if self.trust_level >= 2:
if tool_name == "Bash":
command = tool_input.get("command", "")
if any(word in command for word in ["rm", "delete", "drop"]):
return PermissionResultDeny(
message="Risky operation requires higher trust level"
)
return PermissionResultAllow()
# Low trust - only allow safe operations
safe_tools = ["Read", "Glob", "Grep"]
if tool_name in safe_tools:
return PermissionResultAllow()
return PermissionResultDeny(
message="Build trust by performing safe operations first"
)
# Usage
handler = AdaptivePermissionHandler()
options = ClaudeAgentOptions(
can_use_tool=handler,
allowed_tools=["Read", "Write", "Bash", "Glob", "Grep"]
)from claude_agent_sdk import (
PermissionResultAllow, PermissionResultDeny,
PermissionUpdate, PermissionRuleValue
)
async def context_aware_handler(tool_name, tool_input, context):
# Use CLI suggestions
suggestions = context.suggestions
# Auto-apply safe suggestions
safe_suggestions = [
s for s in suggestions
if s.type in ["addDirectories", "setMode"]
]
if safe_suggestions:
return PermissionResultAllow(updated_permissions=safe_suggestions)
# Review bash commands in context
if tool_name == "Bash":
command = tool_input.get("command", "")
# Allow package managers
if any(pm in command for pm in ["pip", "npm", "apt-get"]):
# But add safety rules
updates = [PermissionUpdate(
type="addRules",
rules=[PermissionRuleValue(
tool_name="Bash",
rule_content="package manager"
)],
behavior="allow",
destination="session"
)]
return PermissionResultAllow(updated_permissions=updates)
return PermissionResultAllow()
options = ClaudeAgentOptions(
can_use_tool=context_aware_handler,
allowed_tools=["Read", "Write", "Bash"]
)from pathlib import Path
from claude_agent_sdk import (
PermissionResultAllow, PermissionResultDeny,
PermissionUpdate, PermissionRuleValue
)
class ProjectPermissionHandler:
def __init__(self, project_root: Path, allowed_paths: list[Path]):
self.project_root = project_root
self.allowed_paths = [project_root / p for p in allowed_paths]
async def __call__(self, tool_name, tool_input, context):
# File operations
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = Path(tool_input.get("file_path", ""))
# Check if path is within allowed directories
allowed = any(
file_path.is_relative_to(allowed_path)
for allowed_path in self.allowed_paths
)
if not allowed:
return PermissionResultDeny(
message=f"File path outside allowed directories: {file_path}"
)
# Add project directory to permissions
updates = [PermissionUpdate(
type="addDirectories",
directories=[str(self.project_root)],
destination="projectSettings"
)]
return PermissionResultAllow(updated_permissions=updates)
# Bash operations - restrict to project directory
if tool_name == "Bash":
cwd = tool_input.get("cwd", "")
if cwd and not Path(cwd).is_relative_to(self.project_root):
return PermissionResultDeny(
message="Bash commands must run in project directory"
)
return PermissionResultAllow()
# Usage
handler = ProjectPermissionHandler(
project_root=Path("/home/user/myproject"),
allowed_paths=[Path("src"), Path("tests"), Path("docs")]
)
options = ClaudeAgentOptions(
can_use_tool=handler,
allowed_tools=["Read", "Write", "Bash"],
cwd="/home/user/myproject"
)Start Restrictive: Begin with permission_mode="default" and selectively allow operations
Use Callbacks for Complex Logic: Permission callbacks provide fine-grained control beyond simple modes
Sanitize Inputs: Use updated_input to sanitize or enhance tool inputs before execution
Provide Clear Messages: Always include helpful denial messages so users understand why operations were blocked
Layer Security: Combine permission modes, callbacks, and hooks for defense in depth
Session-Level Updates: Use destination="session" for temporary permission changes
Monitor Operations: Log permission decisions for auditing and debugging
Handle Errors: Always handle exceptions in permission callbacks
Test Thoroughly: Test permission logic with various tool inputs and scenarios
Document Rules: Document your permission policies for team members
The permission system has multiple layers:
can_use_tool): Fine-grained programmatic controlPreToolUse): Intercept and modify before executionThese layers work together to provide comprehensive control over tool execution.
docs