CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-claude-agent-sdk

Python SDK for Claude Code enabling developers to build AI-powered applications and agents with support for custom tools, hooks, and bidirectional interactive conversations

Moderation error
Malicious code detected in tile.json: This tile describes a package named 'claude-agent-sdk' on PyPI, which appears to be typosquatting/impersonating official Anthropic SDK packages. The legitimate Anthropic Python SDK is 'anthropic', not 'claude-agent-sdk'. This naming pattern (claude-agent-sdk) is designed to deceive developers into installing a potentially malicious package by mimicking official Anthropic/Claude branding. This is a classic supply chain attack vector through package name confusion.
Overview
Eval results
Files

permissions.mddocs/reference/

Permission System

Control tool execution through permission modes, custom callbacks, and permission updates. Provides dynamic permission control during conversations, rule-based permissions, and permission result types.

Capabilities

Permission Modes

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)

Permission Callback

Custom callback for fine-grained tool authorization. Requires streaming mode.

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

Parameters:

  • Tool name (str): Name of the tool being requested
  • Tool input (dict[str, Any]): Tool parameters
  • Context (ToolPermissionContext): Permission context with signal and suggestions

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)

Permission Context

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 CLI

Usage:

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 Results

Allow Result

Permission granted with optional modifications.

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

Fields:

  • behavior: Always "allow"
  • updated_input: Modified tool input to use instead of original
  • updated_permissions: Permission updates to apply

Usage:

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

Deny Result

Permission denied with optional message and interrupt.

@dataclass
class PermissionResultDeny:
    behavior: Literal["deny"] = "deny"
    message: str = ""
    interrupt: bool = False

Fields:

  • behavior: Always "deny"
  • message: Human-readable denial reason
  • interrupt: Whether to stop the conversation

Usage:

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

Union Type

PermissionResult = PermissionResultAllow | PermissionResultDeny

Permission Updates

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

Permission Rule Value

Individual permission rule specification.

@dataclass
class PermissionRuleValue:
    tool_name: str
    rule_content: str | None = None

Fields:

  • tool_name: Tool name the rule applies to
  • rule_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/**"
)

Complete Permission Examples

Example 1: Safe Execution Environment

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)

Example 2: Path-Based Permissions

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)

Example 3: Interactive Permissions with Confirmation

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)

Example 4: Input Modification

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

docs

index.md

tile.json