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

client.mddocs/reference/

Interactive Client

The ClaudeSDKClient class enables bidirectional, interactive conversations with Claude Code. It provides full control over conversation flow with support for streaming, interrupts, dynamic message sending, and session management.

Capabilities

Client Initialization

Create a client instance for interactive conversations.

class ClaudeSDKClient:
    """Interactive client for bidirectional conversations with Claude Code."""
    
    def __init__(
        self,
        options: ClaudeAgentOptions | None = None,
        transport: Transport | None = None,
    ):
        """Initialize client.
        
        Args:
            options: Optional ClaudeAgentOptions configuration. None uses defaults
            transport: Optional custom Transport implementation. None uses bundled CLI
        """

Parameters:

  • options (ClaudeAgentOptions | None): Optional ClaudeAgentOptions configuration. Controls tools, permissions, model, environment, MCP servers, hooks, and more. None uses default configuration
  • transport (Transport | None): Optional custom Transport implementation. None uses default subprocess transport with bundled Claude Code CLI

Usage:

Basic initialization:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    client = ClaudeSDKClient()
    # Use client...

anyio.run(main)

With options:

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        system_prompt="You are a helpful assistant",
        cwd="/path/to/project",
        allowed_tools=["Read", "Write", "Edit"],
        permission_mode="acceptEdits"
    )

    client = ClaudeSDKClient(options=options)
    # Use client...

anyio.run(main)

As context manager (recommended):

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Write", "Bash"],
        permission_mode="acceptEdits"
    )
    
    async with ClaudeSDKClient(options=options) as client:
        # Client is automatically connected and disconnected
        await client.query("Hello")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Connection Management

Establish and close connections to Claude Code.

async def connect(
    self,
    prompt: str | AsyncIterable[dict[str, Any]] | None = None
) -> None:
    """Establish connection to Claude Code.
    
    Args:
        prompt: Optional initial prompt to send immediately after connecting.
                Can be string or async iterable of message dictionaries
                
    Raises:
        CLINotFoundError: Claude Code CLI not found or not installed
        CLIConnectionError: Unable to connect to Claude Code
        ProcessError: CLI process failed to start
    """

Parameters:

  • prompt (str | AsyncIterable[dict[str, Any]] | None): Optional initial prompt. None connects without sending message

Usage:

Connect without initial prompt:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    client = ClaudeSDKClient()
    await client.connect()
    # Now ready to send messages
    
anyio.run(main)

Connect with initial prompt:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    client = ClaudeSDKClient()
    await client.connect(prompt="What files are in the current directory?")
    async for msg in client.receive_response():
        print(msg)
    
anyio.run(main)

Disconnect:

async def disconnect(self) -> None:
    """Close connection and cleanup resources.
    
    Closes the transport connection and performs cleanup. Safe to call multiple times.
    """
import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    client = ClaudeSDKClient()
    await client.connect()
    # ... use client ...
    await client.disconnect()
    
anyio.run(main)

Sending Messages

Send new requests in interactive mode.

async def query(
    self,
    prompt: str | AsyncIterable[dict[str, Any]],
    session_id: str = "default"
) -> None:
    """Send message in interactive mode.
    
    Args:
        prompt: String message or async iterable of message dictionaries
        session_id: Session identifier for conversation tracking (default: "default")
        
    Note: Call receive_messages() or receive_response() to get Claude's responses
    """

Parameters:

  • prompt (str | AsyncIterable[dict[str, Any]]): Message to send. String for simple messages, AsyncIterable for advanced usage
  • session_id (str): Session identifier for tracking conversations. Default: "default". Use different IDs for parallel conversations

Usage:

Send string message:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        await client.query("What is the capital of France?")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Send follow-up:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        # First message
        await client.query("List the Python files")
        async for msg in client.receive_response():
            print(msg)
        
        # Follow-up (has context from first message)
        await client.query("Tell me more about main.py")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Multiple sessions:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        # Session 1
        await client.query("Question for session 1", session_id="session1")
        async for msg in client.receive_response():
            print(f"[Session 1] {msg}")
        
        # Session 2 (independent conversation)
        await client.query("Question for session 2", session_id="session2")
        async for msg in client.receive_response():
            print(f"[Session 2] {msg}")

anyio.run(main)

Receiving Messages

Receive messages from Claude in various modes.

Receive all messages:

async def receive_messages(self) -> AsyncIterator[Message]:
    """Receive all messages continuously.
    
    Yields messages until connection is closed. Use for long-running sessions
    or when you need to process messages as they arrive without stopping.
    
    Yields:
        Message: UserMessage, AssistantMessage, SystemMessage, ResultMessage, or StreamEvent
    """
import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        await client.query("Start working")
        
        # Process all messages until disconnected
        async for message in client.receive_messages():
            print(message)

anyio.run(main)

Receive until result (recommended for most use cases):

async def receive_response(self) -> AsyncIterator[Message]:
    """Receive messages until ResultMessage, then stop.
    
    Yields messages for current query until ResultMessage is encountered,
    then stops. Use this for typical request-response patterns.
    
    Yields:
        Message: Messages for current query, ending with ResultMessage
    """
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient,
    AssistantMessage,
    TextBlock,
    ResultMessage
)

async def main():
    async with ClaudeSDKClient() as client:
        await client.query("Create a hello.py file")
        
        # Process messages until result
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
            elif isinstance(msg, ResultMessage):
                print(f"Completed in {msg.duration_ms}ms")

anyio.run(main)

Interrupting Execution

Send interrupt signal to Claude.

async def interrupt(self) -> dict[str, Any]:
    """Send interrupt signal to stop current operation.
    
    Stops Claude from continuing the current task. Useful for long-running
    operations that need to be cancelled.
    
    Returns:
        dict[str, Any]: Response dictionary with interrupt acknowledgment
    """

Returns: Response dictionary confirming interrupt

Usage:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        # Start a long-running query
        await client.query("Analyze all files in this large repository")
        
        # Interrupt if needed (e.g., based on user input or timeout)
        result = await client.interrupt()
        print(f"Interrupted: {result}")

anyio.run(main)

With timeout:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        await client.query("Long running task")
        
        try:
            with anyio.fail_after(30):  # 30 second timeout
                async for msg in client.receive_response():
                    print(msg)
        except TimeoutError:
            print("Operation timed out, interrupting...")
            await client.interrupt()

anyio.run(main)

Dynamic Permission Control

Change permission mode during active conversation. See Permission System for detailed information about permission modes and control.

async def set_permission_mode(self, mode: PermissionMode) -> dict[str, Any]:
    """Change permission mode mid-conversation.
    
    Args:
        mode: New permission mode. One of:
              - "default": CLI prompts user for dangerous tools
              - "acceptEdits": Auto-accept file edit operations
              - "plan": Plan mode for design/planning workflows
              - "bypassPermissions": Allow all tools (use with caution!)
    
    Returns:
        dict[str, Any]: Response dictionary confirming mode change
    """

Parameters:

  • mode (PermissionMode): New permission mode. One of: "default", "acceptEdits", "plan", "bypassPermissions"

Returns: Response dictionary confirming mode change

Usage:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    # Start with default permissions
    async with ClaudeSDKClient() as client:
        # Review code (read-only operations)
        await client.query("Review this code")
        async for msg in client.receive_response():
            print(msg)

        # Switch to auto-accept edits for implementation
        await client.set_permission_mode("acceptEdits")

        # Now implement fixes (will auto-approve edits)
        await client.query("Now fix the issues you found")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Workflow with permission changes:

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Write", "Edit", "Bash"],
        permission_mode="default"  # Start conservative
    )
    
    async with ClaudeSDKClient(options=options) as client:
        # Phase 1: Analysis (default permissions)
        await client.query("Analyze security issues")
        async for msg in client.receive_response():
            print(msg)
        
        # Phase 2: Planning (plan mode)
        await client.set_permission_mode("plan")
        await client.query("Create a fix plan")
        async for msg in client.receive_response():
            print(msg)
        
        # Phase 3: Implementation (accept edits)
        await client.set_permission_mode("acceptEdits")
        await client.query("Implement the fixes")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Model Switching

Switch AI model mid-conversation.

async def set_model(self, model: str) -> dict[str, Any]:
    """Switch AI model during conversation.
    
    Args:
        model: Model identifier. Can be short name ("sonnet", "opus", "haiku")
               or full model ID (e.g., "claude-sonnet-4-5")
    
    Returns:
        dict[str, Any]: Response dictionary confirming model change
    """

Parameters:

  • model (str): Model identifier. Short names: "sonnet", "opus", "haiku". Full IDs: "claude-sonnet-4-5", "claude-opus-4-5", "claude-haiku-4"

Returns: Response dictionary confirming model change

Usage:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        # Start with default model (usually sonnet)
        await client.query("Simple question")
        async for msg in client.receive_response():
            print(msg)

        # Switch to more powerful model for complex task
        await client.set_model("opus")

        await client.query("Complex reasoning task requiring deep analysis")
        async for msg in client.receive_response():
            print(msg)
        
        # Switch to faster/cheaper model for simple tasks
        await client.set_model("haiku")
        
        await client.query("Quick summary")
        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

File Checkpointing

Rewind tracked files to their state at a specific user message. Requires enable_file_checkpointing=True in options.

async def rewind_files(self, user_message_id: str) -> dict[str, Any]:
    """Rewind tracked files to previous state.
    
    Args:
        user_message_id: UUID of user message to rewind to. Get from UserMessage.uuid
        
    Returns:
        dict[str, Any]: Response dictionary with rewind results
        
    Requires: enable_file_checkpointing=True in ClaudeAgentOptions
    
    Note: Only files modified during the session are tracked and can be rewound
    """

Parameters:

  • user_message_id (str): UUID of user message to rewind to. Obtained from UserMessage.uuid

Returns: Response dictionary with rewind results

Requires: enable_file_checkpointing=True in ClaudeAgentOptions

Usage:

import anyio
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    UserMessage
)

async def main():
    options = ClaudeAgentOptions(
        enable_file_checkpointing=True,
        allowed_tools=["Read", "Write", "Edit"],
        permission_mode="acceptEdits"
    )

    async with ClaudeSDKClient(options=options) as client:
        # Make initial changes
        await client.query("Modify config.json")
        message_id = None
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and msg.uuid:
                message_id = msg.uuid

        # Make more changes
        await client.query("Make more changes to config.json")
        async for msg in client.receive_response():
            print(msg)

        # Rewind files to previous state
        if message_id:
            result = await client.rewind_files(message_id)
            print(f"Rewound files: {result}")
            
            # Files are now back to state after first query

anyio.run(main)

MCP Server Status

Query MCP server initialization status.

async def get_mcp_status(self) -> dict[str, Any]:
    """Query MCP server initialization status.
    
    Returns:
        dict[str, Any]: Status dictionary with server states showing which
                        MCP servers are initialized, failed, or in progress
    """

Returns: Status dictionary with server states

Usage:

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def main():
    options = ClaudeAgentOptions(
        mcp_servers={
            "calculator": {
                "type": "stdio",
                "command": "calc-server"
            }
        }
    )
    
    async with ClaudeSDKClient(options=options) as client:
        status = await client.get_mcp_status()
        print(f"MCP servers: {status}")

anyio.run(main)

Server Information

Get server initialization information.

async def get_server_info(self) -> dict[str, Any] | None:
    """Get server initialization information.
    
    Returns:
        dict[str, Any] | None: Server info dictionary with capabilities and version,
                               or None if not available
    """

Returns: Server info dictionary or None if not available

Usage:

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        info = await client.get_server_info()
        if info:
            print(f"Server capabilities: {info}")

anyio.run(main)

Complete Example

import anyio
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    AssistantMessage,
    TextBlock,
    ResultMessage,
    ToolUseBlock
)

async def interactive_session():
    """Complete interactive session example."""
    options = ClaudeAgentOptions(
        system_prompt="You are a helpful coding assistant",
        cwd="/path/to/project",
        allowed_tools=["Read", "Write", "Bash"],
        permission_mode="default"  # Start with confirmations
    )

    async with ClaudeSDKClient(options=options) as client:
        # First query
        print("\n=== Query 1: List files ===")
        await client.query("What files are in this directory?")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
                    elif isinstance(block, ToolUseBlock):
                        print(f"[Tool: {block.name}]")
            elif isinstance(msg, ResultMessage):
                print(f"Duration: {msg.duration_ms}ms")

        # Follow-up query (has context)
        print("\n=== Query 2: Read file ===")
        await client.query("Read the README.md file")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

        # Change permissions for implementation
        print("\n=== Switching to acceptEdits mode ===")
        await client.set_permission_mode("acceptEdits")
        
        await client.query("Add a new feature to main.py")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

anyio.run(interactive_session)

When to Use ClaudeSDKClient

Use ClaudeSDKClient for:

  • Interactive conversations: Multi-turn dialogs with context
  • Chat applications: Building chat UIs or conversational interfaces
  • Reactive responses: When you need to react to Claude's responses
  • Long-running sessions: Sessions with multiple related queries
  • Dynamic control: Need to change permissions or model mid-conversation
  • Interrupt capability: May need to stop long-running operations
  • Real-time applications: Applications with user input during execution
  • Exploratory sessions: Interactive debugging or exploration

Use query() instead for:

  • Simple one-off questions: Single question, get answer, done
  • Batch processing: Processing independent prompts
  • Fire-and-forget automation: Automated scripts with known inputs
  • Stateless operations: No need for conversation context
  • All inputs known upfront: No decisions based on responses

Common Patterns

Pattern 1: Multi-Turn Conversation

import anyio
from claude_agent_sdk import ClaudeSDKClient, AssistantMessage, TextBlock

async def main():
    async with ClaudeSDKClient() as client:
        # Turn 1
        await client.query("What programming languages are you familiar with?")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
        
        # Turn 2 (has context from Turn 1)
        await client.query("Which one would you recommend for web development?")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
        
        # Turn 3 (has context from Turn 1 and 2)
        await client.query("Can you show me a hello world example?")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(block.text)

anyio.run(main)

Pattern 2: Chat REPL

import anyio
from claude_agent_sdk import ClaudeSDKClient, AssistantMessage, TextBlock

async def chat_repl():
    """Interactive REPL-style chat."""
    async with ClaudeSDKClient() as client:
        print("Chat started. Type 'exit' to quit.")
        
        while True:
            # Get user input
            user_input = input("\nYou: ").strip()
            if user_input.lower() == 'exit':
                break
            
            # Send to Claude
            await client.query(user_input)
            
            print("Claude: ", end="")
            async for msg in client.receive_response():
                if isinstance(msg, AssistantMessage):
                    for block in msg.content:
                        if isinstance(block, TextBlock):
                            print(block.text, end=" ")
            print()  # New line

anyio.run(chat_repl)

Pattern 3: Progressive Workflow

import anyio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

async def progressive_workflow():
    """Multi-phase workflow with permission escalation."""
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Write", "Edit", "Bash"],
        permission_mode="default"
    )
    
    async with ClaudeSDKClient(options=options) as client:
        # Phase 1: Analysis (read-only effectively)
        print("Phase 1: Analysis")
        await client.query("Analyze the codebase for issues")
        async for msg in client.receive_response():
            print(msg)
        
        # Phase 2: Planning
        print("\nPhase 2: Planning")
        await client.set_permission_mode("plan")
        await client.query("Create a detailed plan to fix the issues")
        async for msg in client.receive_response():
            print(msg)
        
        # User review happens here...
        print("\n[User reviews plan]")
        
        # Phase 3: Implementation
        print("\nPhase 3: Implementation")
        await client.set_permission_mode("acceptEdits")
        await client.query("Implement the fixes according to the plan")
        async for msg in client.receive_response():
            print(msg)

anyio.run(progressive_workflow)

Pattern 4: Error Recovery with Checkpoints

import anyio
from claude_agent_sdk import (
    ClaudeSDKClient,
    ClaudeAgentOptions,
    UserMessage,
    ResultMessage
)

async def main():
    options = ClaudeAgentOptions(
        enable_file_checkpointing=True,
        permission_mode="acceptEdits"
    )
    
    checkpoints = []
    
    async with ClaudeSDKClient(options=options) as client:
        # Checkpoint 1: Initial implementation
        await client.query("Implement feature A")
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and msg.uuid:
                checkpoints.append(("feature_a", msg.uuid))
        
        # Checkpoint 2: Add feature B
        await client.query("Add feature B")
        async for msg in client.receive_response():
            if isinstance(msg, UserMessage) and msg.uuid:
                checkpoints.append(("feature_b", msg.uuid))
        
        # Try risky change
        await client.query("Refactor everything")
        error_occurred = False
        async for msg in client.receive_response():
            if isinstance(msg, ResultMessage) and msg.is_error:
                error_occurred = True
        
        # Rewind on error
        if error_occurred and checkpoints:
            print("Error occurred! Rewinding to last checkpoint...")
            name, checkpoint_id = checkpoints[-1]
            await client.rewind_files(checkpoint_id)
            print(f"Rewound to checkpoint: {name}")

anyio.run(main)

Pattern 5: Parallel Sessions

import anyio
from claude_agent_sdk import ClaudeSDKClient

async def process_session(client, session_id: str, task: str):
    """Process a single session."""
    await client.query(task, session_id=session_id)
    async for msg in client.receive_response():
        print(f"[{session_id}] {msg}")

async def main():
    """Run parallel sessions."""
    async with ClaudeSDKClient() as client:
        # Start multiple tasks in parallel sessions
        async with anyio.create_task_group() as tg:
            tg.start_soon(process_session, client, "docs", "Document the API")
            tg.start_soon(process_session, client, "tests", "Write unit tests")
            tg.start_soon(process_session, client, "refactor", "Refactor utils")

anyio.run(main)

Troubleshooting

Issue: Client hangs after query

Cause: Not calling receive_response() or receive_messages()
Solution: Always call receive methods after query to process responses

Issue: Context not maintained between queries

Cause: Using different session_ids or creating new clients
Solution: Use same client instance and session_id for related queries

Issue: Can't interrupt long operations

Cause: Not using ClaudeSDKClient (query() doesn't support interrupts)
Solution: Use ClaudeSDKClient and call interrupt() method

Issue: Permission mode change doesn't apply

Cause: set_permission_mode called before connection or after error
Solution: Ensure client is connected and in valid state before changing mode

Issue: File checkpointing not working

Cause: enable_file_checkpointing not set or no file operations occurred
Solution: Set enable_file_checkpointing=True in options before file operations

Performance Tips

Use context manager: Always use async with to ensure proper cleanup

Receive responses: Always call receive_response() after query() to avoid blocking

Handle errors: Wrap in try/except to handle connection errors gracefully

Monitor sessions: Track session_ids for parallel conversations

Limit tools: Restrict allowed_tools to only what's needed for performance

See Also

Install with Tessl CLI

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

docs

index.md

tile.json