CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-claude-agent-sdk

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

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

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

Overview
Eval results
Files

permission-control.mddocs/

Permission Control

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.

Capabilities

Permission Modes

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"]
)

Dynamic Permission Mode Changes

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')

Permission Callback

Programmatic permission control via callback function.

CanUseTool = Callable[
    [str, dict[str, Any], ToolPermissionContext],
    Awaitable[PermissionResult]
]

Parameters:

  1. tool_name (str): Name of the tool being invoked
  2. tool_input (dict[str, Any]): Tool input parameters
  3. context (ToolPermissionContext): Permission context with signal and suggestions

Returns:

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"]
)

Tool Permission Context

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 Result Types

Permission decisions returned from callbacks.

Allow Result

@dataclass
class PermissionResultAllow:
    """Allow permission decision."""

    behavior: Literal["allow"] = "allow"
    updated_input: dict[str, Any] | None = None
    updated_permissions: list[PermissionUpdate] | None = None

Fields:

  • 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
)

Deny Result

@dataclass
class PermissionResultDeny:
    """Deny permission decision."""

    behavior: Literal["deny"] = "deny"
    message: str = ""
    interrupt: bool = False

Fields:

  • 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
)

Permission Result Union

PermissionResult = PermissionResultAllow | PermissionResultDeny

Permission Update

Configure 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 only

Methods:

  • 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()

Permission Rule Value

Individual permission rule.

@dataclass
class PermissionRuleValue:
    """Permission rule value."""

    tool_name: str
    rule_content: str | None = None

Fields:

  • 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 tools

Permission Behavior

Rule behavior types.

PermissionBehavior = Literal["allow", "deny", "ask"]

Values:

  • "allow": Allow the operation
  • "deny": Deny the operation
  • "ask": Prompt the user

Permission Update Destination

Where 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)

Complete Examples

Security-Aware Permission Handler

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)

Adaptive Permission Handler

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"]
)

Context-Aware Permission Handler

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"]
)

Project-Based Permission Handler

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"
)

Best Practices

  1. Start Restrictive: Begin with permission_mode="default" and selectively allow operations

  2. Use Callbacks for Complex Logic: Permission callbacks provide fine-grained control beyond simple modes

  3. Sanitize Inputs: Use updated_input to sanitize or enhance tool inputs before execution

  4. Provide Clear Messages: Always include helpful denial messages so users understand why operations were blocked

  5. Layer Security: Combine permission modes, callbacks, and hooks for defense in depth

  6. Session-Level Updates: Use destination="session" for temporary permission changes

  7. Monitor Operations: Log permission decisions for auditing and debugging

  8. Handle Errors: Always handle exceptions in permission callbacks

  9. Test Thoroughly: Test permission logic with various tool inputs and scenarios

  10. Document Rules: Document your permission policies for team members

Permission System Architecture

The permission system has multiple layers:

  1. Permission Mode: Coarse-grained control (default, acceptEdits, plan, bypassPermissions)
  2. Allowed/Disallowed Tools: Tool-level filtering
  3. Permission Callbacks (can_use_tool): Fine-grained programmatic control
  4. Hooks (PreToolUse): Intercept and modify before execution
  5. Permission Updates: Dynamic rule changes during execution

These layers work together to provide comprehensive control over tool execution.

docs

agent-definitions.md

agents.md

client.md

configuration-options.md

content-blocks.md

core-query-interface.md

custom-tools.md

error-handling.md

errors.md

hook-system.md

hooks.md

index.md

mcp-config.md

mcp-server-configuration.md

messages-and-content.md

messages.md

options.md

permission-control.md

permissions.md

query.md

transport.md

COMPLETION_SUMMARY.md

tile.json