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
Permission control provides fine-grained control over which tools Claude can execute. You can use permission modes for broad control or implement custom permission callbacks for precise per-tool decisions.
Predefined permission modes for controlling tool execution.
PermissionMode = Literal["default", "acceptEdits", "plan", "bypassPermissions"]
"""
Permission modes for tool execution.
Permission modes provide preset configurations for how Claude handles
tool permissions:
- 'default': Prompt user for dangerous operations (interactive mode)
- 'acceptEdits': Auto-accept file edits, prompt for other dangerous operations
- 'plan': Generate execution plan without actually executing tools
- 'bypassPermissions': Allow all tools without prompting (use with extreme caution)
Used in ClaudeAgentOptions.permission_mode.
"""Result types returned by permission callbacks.
PermissionResult = PermissionResultAllow | PermissionResultDeny
"""
Union of permission result types.
Permission callbacks return either PermissionResultAllow or PermissionResultDeny
to indicate whether a tool should be allowed or blocked.
"""Allow a tool to execute, optionally with modifications.
@dataclass
class PermissionResultAllow:
"""
Allow permission result.
Indicates that a tool should be allowed to execute. Optionally includes
modifications to the tool input or permission updates.
Attributes:
behavior: Always "allow"
updated_input: Modified tool input parameters
updated_permissions: Permission updates to apply
"""
behavior: Literal["allow"] = "allow"
"""Behavior marker.
Always set to "allow" to indicate permission is granted.
"""
updated_input: dict[str, Any] | None = None
"""Modified tool input.
If set, the tool will be executed with these input parameters instead
of the original ones. Allows you to modify tool behavior on-the-fly.
Example:
# Original input: {"command": "rm -rf /"}
# Modified input: {"command": "echo 'blocked dangerous command'"}
updated_input={"command": "echo 'blocked dangerous command'"}
"""
updated_permissions: list[PermissionUpdate] | None = None
"""Permission updates to apply.
List of permission updates to apply after allowing this tool use.
Can add rules, change modes, update directories, etc.
See PermissionUpdate for details.
"""Deny a tool execution.
@dataclass
class PermissionResultDeny:
"""
Deny permission result.
Indicates that a tool should not be allowed to execute. Includes
a message explaining why and whether to interrupt the conversation.
Attributes:
behavior: Always "deny"
message: Explanation for denial
interrupt: Whether to interrupt the conversation
"""
behavior: Literal["deny"] = "deny"
"""Behavior marker.
Always set to "deny" to indicate permission is denied.
"""
message: str = ""
"""Denial message.
Human-readable explanation of why the tool was blocked. This message
may be shown to the user or logged.
Example:
"Dangerous command blocked for security reasons"
"Tool not available in readonly mode"
"""
interrupt: bool = False
"""Interrupt flag.
If True, the conversation will be interrupted immediately after denying
this tool. If False, Claude may try alternative approaches.
Use True for critical security issues, False for normal denials.
"""Configuration for updating permissions during execution.
@dataclass
class PermissionUpdate:
"""
Permission update configuration.
Defines a change to permission settings. Can add/remove rules, change
modes, or modify directory access. Used in PermissionResultAllow.
Attributes:
type: Type of permission update
rules: Permission rules (for rule-based updates)
behavior: Permission behavior (for rule-based updates)
mode: Permission mode (for mode updates)
directories: Directories (for directory updates)
destination: Where to save the update
"""
type: Literal[
"addRules",
"replaceRules",
"removeRules",
"setMode",
"addDirectories",
"removeDirectories",
]
"""Update type.
Specifies what kind of permission change to make:
- 'addRules': Add new permission rules
- 'replaceRules': Replace existing permission rules
- 'removeRules': Remove permission rules
- 'setMode': Change the permission mode
- 'addDirectories': Add directories to allowed list
- 'removeDirectories': Remove directories from allowed list
"""
rules: list[PermissionRuleValue] | None = None
"""Permission rules.
List of rules to add, replace, or remove. Required for rule-based
update types (addRules, replaceRules, removeRules).
See PermissionRuleValue for rule structure.
"""
behavior: PermissionBehavior | None = None
"""Permission behavior.
Behavior to apply to rules: "allow", "deny", or "ask".
Required for rule-based update types.
"""
mode: PermissionMode | None = None
"""Permission mode.
The mode to set. Required for 'setMode' update type.
"""
directories: list[str] | None = None
"""Directories list.
List of directory paths. Required for directory-based update types
(addDirectories, removeDirectories).
"""
destination: PermissionUpdateDestination | None = None
"""Update destination.
Where to save this permission update:
- 'userSettings': User-level settings
- 'projectSettings': Project-level settings
- 'localSettings': Local settings
- 'session': Current session only
If None, applies to current session only.
"""
def to_dict(self) -> dict[str, Any]:
"""
Convert PermissionUpdate to dictionary format.
Converts the dataclass to a dictionary matching the TypeScript
control protocol format.
Returns:
Dictionary representation of the permission update
"""Individual permission rule configuration.
@dataclass
class PermissionRuleValue:
"""
Permission rule value.
Defines a single permission rule for a tool.
Attributes:
tool_name: Name of the tool this rule applies to
rule_content: Optional rule details or pattern
"""
tool_name: str
"""Tool name.
The name of the tool this rule applies to. Examples:
- "Bash"
- "Write"
- "Read"
- "custom_tool"
"""
rule_content: str | None = None
"""Rule content.
Optional additional details about the rule. Can be:
- A pattern to match against tool input
- A condition to evaluate
- Additional metadata
Format depends on the tool and use case.
"""Behavior types for permission rules.
PermissionBehavior = Literal["allow", "deny", "ask"]
"""
Permission behaviors for rules.
- 'allow': Always allow the tool
- 'deny': Always deny the tool
- 'ask': Prompt the user for decision
"""Where to save permission updates.
PermissionUpdateDestination = Literal[
"userSettings", "projectSettings", "localSettings", "session"
]
"""
Permission update destinations.
Specifies where to persist permission updates:
- 'userSettings': Save to user-level settings (all projects)
- 'projectSettings': Save to project-level settings (current project)
- 'localSettings': Save to local settings (current directory)
- 'session': Apply only to current session (not persisted)
"""Context information passed to permission callbacks.
@dataclass
class ToolPermissionContext:
"""
Context information for tool permission callbacks.
Provides additional context when making permission decisions.
Attributes:
signal: Abort signal support (future feature)
suggestions: Permission suggestions from CLI
"""
signal: Any | None = None
"""Future: abort signal support.
Reserved for future use to allow canceling permission checks.
Currently always None.
"""
suggestions: list[PermissionUpdate] = field(default_factory=list)
"""Permission suggestions from CLI.
The CLI may provide suggested permission updates based on the
tool use context. Your callback can choose to apply these
suggestions or make its own decisions.
"""Type alias for tool permission callbacks.
CanUseTool = Callable[
[str, dict[str, Any], ToolPermissionContext],
Awaitable[PermissionResult]
]
"""
Tool permission callback type.
An async function that determines whether a specific tool use should
be allowed. Called before each tool execution.
Args:
tool_name: Name of the tool Claude wants to use
tool_input: Input parameters for the tool
context: Additional context including suggestions
Returns:
PermissionResult (either PermissionResultAllow or PermissionResultDeny)
Example:
async def can_use_tool(
tool_name: str,
tool_input: dict[str, Any],
context: ToolPermissionContext
) -> PermissionResult:
if tool_name == "Bash" and "rm" in tool_input.get("command", ""):
return PermissionResultDeny(
message="Dangerous command blocked",
interrupt=False
)
return PermissionResultAllow()
"""from claude_agent_sdk import ClaudeAgentOptions, query
# Default mode - prompt for dangerous operations
options = ClaudeAgentOptions(
permission_mode='default',
allowed_tools=["Read", "Write", "Bash"]
)
# Accept edits mode - auto-accept file changes
options = ClaudeAgentOptions(
permission_mode='acceptEdits',
allowed_tools=["Read", "Write", "Edit"]
)
# Plan mode - generate plan without execution
options = ClaudeAgentOptions(
permission_mode='plan',
allowed_tools=["Read", "Write", "Bash"]
)
# Bypass permissions - allow everything (use with caution!)
options = ClaudeAgentOptions(
permission_mode='bypassPermissions',
allowed_tools=["Read", "Write", "Bash"]
)
async for msg in query(prompt="Modify files", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
async def can_use_tool(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
) -> PermissionResult:
"""Simple permission callback."""
# Allow Read operations
if tool_name == "Read":
return PermissionResultAllow()
# Block Bash commands
if tool_name == "Bash":
return PermissionResultDeny(
message="Bash commands not allowed",
interrupt=False
)
# Allow everything else
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
can_use_tool=can_use_tool
)
async for msg in query(prompt="Analyze files", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
async def block_dangerous_commands(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Block dangerous bash commands."""
if tool_name == "Bash":
command = tool_input.get("command", "")
dangerous_patterns = [
"rm -rf",
"dd if=",
"mkfs",
"> /dev/",
"chmod 777"
]
for pattern in dangerous_patterns:
if pattern in command:
return PermissionResultDeny(
message=f"Blocked dangerous command pattern: {pattern}",
interrupt=True # Stop immediately
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
can_use_tool=block_dangerous_commands
)
async for msg in query(prompt="Clean up files", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
)
async def modify_bash_commands(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Modify bash commands to add safety flags."""
if tool_name == "Bash":
command = tool_input.get("command", "")
# Add -i flag to rm commands for interactive prompts
if "rm" in command and "-i" not in command:
modified_command = command.replace("rm ", "rm -i ")
return PermissionResultAllow(
updated_input={"command": modified_command}
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
can_use_tool=modify_bash_commands
)
async for msg in query(prompt="Delete old files", options=options):
print(msg)from pathlib import Path
from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
ALLOWED_PATHS = ["/home/user/project", "/tmp"]
async def enforce_path_restrictions(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Restrict file operations to allowed paths."""
if tool_name in ["Read", "Write", "Edit"]:
file_path = tool_input.get("file_path", "")
abs_path = Path(file_path).resolve()
# Check if path is within allowed directories
allowed = any(
str(abs_path).startswith(str(Path(allowed_path).resolve()))
for allowed_path in ALLOWED_PATHS
)
if not allowed:
return PermissionResultDeny(
message=f"Access denied: {file_path} is outside allowed paths",
interrupt=False
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit"],
can_use_tool=enforce_path_restrictions
)
async for msg in query(prompt="Modify configuration", options=options):
print(msg)from datetime import datetime
from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
async def business_hours_only(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Only allow certain tools during business hours."""
hour = datetime.now().hour
# Restrict Bash during off-hours (before 9am or after 5pm)
if tool_name == "Bash" and (hour < 9 or hour >= 17):
return PermissionResultDeny(
message="Bash commands only allowed during business hours (9am-5pm)",
interrupt=False
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Read", "Write"],
can_use_tool=business_hours_only
)
async for msg in query(prompt="Run deployment script", options=options):
print(msg)import logging
from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
)
logging.basicConfig(level=logging.INFO)
async def log_and_allow(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Log all tool uses and allow them."""
logging.info(f"Tool use: {tool_name}")
logging.info(f"Input: {tool_input}")
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
can_use_tool=log_and_allow
)
async for msg in query(prompt="Analyze project", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
async def ask_user_permission(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Ask user for confirmation on file writes."""
if tool_name in ["Write", "Edit"]:
file_path = tool_input.get("file_path", "unknown")
print(f"\nClaude wants to modify: {file_path}")
print(f"Tool: {tool_name}")
# In a real application, you'd use a proper input method
# This is just for demonstration
response = input("Allow? (y/n): ")
if response.lower() != 'y':
return PermissionResultDeny(
message="User denied permission",
interrupt=False
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Edit"],
can_use_tool=ask_user_permission
)
async for msg in query(prompt="Fix bugs", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionUpdate, PermissionRuleValue, ToolPermissionContext
)
async def add_permission_after_use(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Add permission rule after first use."""
if tool_name == "Read":
# After first read, allow all future reads automatically
return PermissionResultAllow(
updated_permissions=[
PermissionUpdate(
type="addRules",
rules=[PermissionRuleValue(tool_name="Read")],
behavior="allow",
destination="session" # Session only
)
]
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write"],
can_use_tool=add_permission_after_use
)
async for msg in query(prompt="Read multiple files", options=options):
print(msg)from datetime import datetime, timedelta
from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
# Rate limiting state
tool_usage = {}
RATE_LIMIT = 10 # Max 10 uses per minute
async def rate_limit_tools(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Rate limit tool usage."""
if tool_name not in tool_usage:
tool_usage[tool_name] = []
now = datetime.now()
# Remove old entries
tool_usage[tool_name] = [
ts for ts in tool_usage[tool_name]
if now - ts < timedelta(minutes=1)
]
# Check limit
if len(tool_usage[tool_name]) >= RATE_LIMIT:
return PermissionResultDeny(
message=f"Rate limit exceeded for {tool_name} ({RATE_LIMIT}/min)",
interrupt=False
)
# Record usage
tool_usage[tool_name].append(now)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
can_use_tool=rate_limit_tools
)
async for msg in query(prompt="Run many tests", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow,
PermissionResultDeny, ToolPermissionContext
)
class AppState:
def __init__(self):
self.readonly = False
self.trusted = False
state = AppState()
async def conditional_permissions(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Apply conditional permissions based on app state."""
# Block writes in readonly mode
if state.readonly and tool_name in ["Write", "Edit", "Bash"]:
return PermissionResultDeny(
message="System is in readonly mode",
interrupt=False
)
# Allow everything if trusted
if state.trusted:
return PermissionResultAllow()
# Otherwise, apply normal rules
if tool_name == "Bash":
return PermissionResultDeny(
message="Bash not allowed in untrusted mode",
interrupt=False
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"],
can_use_tool=conditional_permissions
)
# Enable readonly mode
state.readonly = True
async for msg in query(prompt="Analyze code", options=options):
print(msg)from claude_agent_sdk import (
ClaudeAgentOptions, query, PermissionResultAllow, ToolPermissionContext
)
async def use_suggestions(
tool_name: str,
tool_input: dict,
context: ToolPermissionContext
):
"""Use permission suggestions from CLI."""
# Check if CLI provided suggestions
if context.suggestions:
print(f"CLI suggests {len(context.suggestions)} permission updates")
# Accept suggestions
return PermissionResultAllow(
updated_permissions=context.suggestions
)
return PermissionResultAllow()
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write"],
can_use_tool=use_suggestions
)
async for msg in query(prompt="Work on project", options=options):
print(msg)docs