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

permission-control.mddocs/guides/

Permission Control Guide

Control tool execution through permission modes and custom callbacks.

Permission Modes

Claude Agent SDK offers four permission modes:

ModeBehaviorUse Case
defaultPrompts for dangerous toolsInteractive sessions with human oversight
acceptEditsAuto-approve file editsAutomation with file operations
planPlanning modeDesign/architecture phase
bypassPermissionsAllow all toolsControlled environments only ⚠️

Basic Permission Configuration

Accept Edits (Recommended for Automation)

from claude_agent_sdk import ClaudeAgentOptions

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Edit"],
    permission_mode="acceptEdits"  # No prompts for file operations
)

Default Mode (Interactive)

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Bash"],
    permission_mode="default"  # Will prompt for dangerous operations
)

Bypass Permissions (Use with Caution)

options = ClaudeAgentOptions(
    permission_mode="bypassPermissions"  # Allow everything - use carefully!
)

Custom Permission Callbacks

For fine-grained control, implement a custom permission callback:

Basic Callback

from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    PermissionResultAllow,
    PermissionResultDeny
)

async def permission_callback(tool_name, tool_input, context):
    # Allow Read operations
    if tool_name == "Read":
        return PermissionResultAllow(behavior="allow")
    
    # Block Bash commands
    if tool_name == "Bash":
        return PermissionResultDeny(
            behavior="deny",
            message="Bash commands not allowed in this session"
        )
    
    # Allow everything else
    return PermissionResultAllow(behavior="allow")

options = ClaudeAgentOptions(
    can_use_tool=permission_callback,
    allowed_tools=["Read", "Write", "Bash"]
)

Command Validation

async def validate_bash_commands(tool_name, tool_input, context):
    if tool_name != "Bash":
        return PermissionResultAllow(behavior="allow")
    
    command = tool_input.get("command", "")
    
    # Block dangerous patterns
    dangerous_patterns = ["rm -rf", "mkfs", "dd if=", "> /dev/"]
    for pattern in dangerous_patterns:
        if pattern in command:
            return PermissionResultDeny(
                behavior="deny",
                message=f"Blocked dangerous command containing: {pattern}",
                interrupt=True  # Stop the entire conversation
            )
    
    return PermissionResultAllow(behavior="allow")

Path-Based Permissions

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

Interactive Confirmation

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")
    
    # Ask for confirmation on 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"
            )
    
    return PermissionResultAllow(behavior="allow")

Modifying Tool Input

You can modify tool inputs before execution:

async def modify_input_callback(tool_name, tool_input, context):
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        
        # Add safety flags to rm commands
        if command.startswith("rm ") and "-i" not in command:
            modified_command = command.replace("rm ", "rm -i ", 1)
            return PermissionResultAllow(
                behavior="allow",
                updated_input={"command": modified_command}
            )
    
    return PermissionResultAllow(behavior="allow")

Permission Updates

Apply permission rules dynamically:

from claude_agent_sdk import PermissionUpdate, PermissionRuleValue

async def dynamic_permission_callback(tool_name, tool_input, context):
    if tool_name == "Read":
        # Allow and add rule for future reads
        return PermissionResultAllow(
            behavior="allow",
            updated_permissions=[
                PermissionUpdate(
                    type="addRules",
                    rules=[
                        PermissionRuleValue(
                            tool_name="Read",
                            rule_content="/project/**/*.py"
                        )
                    ],
                    behavior="allow",
                    destination="session"
                )
            ]
        )
    
    return PermissionResultAllow(behavior="allow")

Dynamic Permission Changes

Change permissions mid-conversation:

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    async with ClaudeSDKClient() as client:
        # Phase 1: Review (default permissions)
        await client.query("Review this code")
        async for msg in client.receive_response():
            print(msg)
        
        # Phase 2: Implementation (accept edits)
        await client.set_permission_mode("acceptEdits")
        await client.query("Now fix the issues")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Complete Example: Secure Environment

import anyio
from pathlib import Path
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    PermissionResultAllow,
    PermissionResultDeny
)

# Configuration
ALLOWED_PATHS = ["/workspace/src", "/workspace/docs"]
BLOCKED_COMMANDS = ["rm -rf", "format", "dd if=", "> /dev/", "mkfs"]
SAFE_TOOLS = ["Read", "Grep", "Glob"]

async def secure_permission_callback(tool_name, tool_input, context):
    """Secure permission callback for production environment."""
    
    # Always allow safe read operations
    if tool_name in SAFE_TOOLS:
        return PermissionResultAllow(behavior="allow")
    
    # Validate Bash commands
    if tool_name == "Bash":
        command = tool_input.get("command", "")
        
        for pattern in BLOCKED_COMMANDS:
            if pattern in command:
                return PermissionResultDeny(
                    behavior="deny",
                    message=f"Security violation: command contains '{pattern}'",
                    interrupt=True
                )
        
        # Log all allowed commands
        print(f"[AUDIT] Bash command: {command}")
        return PermissionResultAllow(behavior="allow")
    
    # Validate file operations
    if tool_name in ["Write", "Edit"]:
        file_path = tool_input.get("file_path", "")
        
        # Check if in allowed paths
        allowed = any(file_path.startswith(p) for p in ALLOWED_PATHS)
        if not allowed:
            return PermissionResultDeny(
                behavior="deny",
                message=f"File operations only allowed in: {', '.join(ALLOWED_PATHS)}"
            )
        
        # Block sensitive files
        if any(s in file_path for s in [".env", "credentials", "secrets"]):
            return PermissionResultDeny(
                behavior="deny",
                message="Cannot modify sensitive configuration files",
                interrupt=True
            )
        
        print(f"[AUDIT] File operation: {tool_name} on {file_path}")
        return PermissionResultAllow(behavior="allow")
    
    # Deny unknown tools
    return PermissionResultDeny(
        behavior="deny",
        message=f"Tool {tool_name} not in approved list"
    )

async def main():
    options = ClaudeAgentOptions(
        can_use_tool=secure_permission_callback,
        allowed_tools=["Read", "Write", "Edit", "Bash", "Grep", "Glob"],
        cwd="/workspace"
    )
    
    async with ClaudeSDKClient(options=options) as client:
        await client.query("Update the documentation and run tests")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Best Practices

1. Use Appropriate Mode for Context

# Development: Allow edits
dev_options = ClaudeAgentOptions(permission_mode="acceptEdits")

# CI/CD: Bypass for automation
ci_options = ClaudeAgentOptions(permission_mode="bypassPermissions")

# Production: Use callbacks for fine control
prod_options = ClaudeAgentOptions(can_use_tool=strict_callback)

2. Whitelist, Don't Blacklist

# Good: Whitelist approach
ALLOWED_TOOLS = ["Read", "Grep", "Glob"]

async def whitelist_callback(tool_name, tool_input, context):
    if tool_name in ALLOWED_TOOLS:
        return PermissionResultAllow(behavior="allow")
    return PermissionResultDeny(behavior="deny", message="Tool not allowed")

# Avoid: Blacklist approach (easy to miss dangerous tools)
BLOCKED_TOOLS = ["Bash"]  # What about other dangerous tools?

3. Log Security Events

import logging

logger = logging.getLogger(__name__)

async def logging_callback(tool_name, tool_input, context):
    logger.info(f"Permission request: {tool_name} with input {tool_input}")
    
    # ... permission logic ...
    
    if denied:
        logger.warning(f"Permission denied: {tool_name}")

4. Fail Secure

async def fail_secure_callback(tool_name, tool_input, context):
    try:
        # Permission logic
        return check_permission(tool_name, tool_input)
    except Exception as e:
        # On error, deny rather than allow
        logger.error(f"Permission check error: {e}")
        return PermissionResultDeny(
            behavior="deny",
            message="Permission check failed"
        )

Common Permission Patterns

Read-Only Mode

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob"],
    permission_mode="default"
)

Safe File Operations

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Write", "Edit"],
    permission_mode="acceptEdits"
)

No Network Access

async def no_network_callback(tool_name, tool_input, context):
    if tool_name in ["WebFetch", "WebSearch"]:
        return PermissionResultDeny(behavior="deny", message="Network access disabled")
    return PermissionResultAllow(behavior="allow")

Audit Trail

audit_log = []

async def audit_callback(tool_name, tool_input, context):
    audit_log.append({
        "tool": tool_name,
        "input": tool_input,
        "timestamp": datetime.now()
    })
    return PermissionResultAllow(behavior="allow")

Next Steps

  • Interactive sessions: Interactive Sessions Guide
  • Real examples: Real-World Scenarios
  • Permission reference: Permissions Reference

Install with Tessl CLI

npx tessl i tessl/pypi-claude-agent-sdk@0.1.3

docs

index.md

tile.json