CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-claude-code-sdk

Python SDK for Claude Code providing simple query functions and advanced bidirectional interactive conversations with custom tool support

Overview
Eval results
Files

interactive-client.mddocs/

Interactive Client

The ClaudeSDKClient provides bidirectional, interactive conversations with Claude Code. This client offers full control over the conversation flow with support for streaming, interrupts, dynamic message sending, custom tools, and hooks.

Capabilities

Client Class

Main client class for interactive conversations with full control over message flow and advanced features.

class ClaudeSDKClient:
    """
    Client for bidirectional, interactive conversations with Claude Code.

    Key features:
    - Bidirectional: Send and receive messages at any time
    - Stateful: Maintains conversation context across messages
    - Interactive: Send follow-ups based on responses
    - Control flow: Support for interrupts and session management
    """

    def __init__(self, options: ClaudeCodeOptions | None = None):
        """
        Initialize Claude SDK client.

        Args:
            options: Optional configuration (defaults to ClaudeCodeOptions() if None)
        """

Connection Management

Establish and manage connections to Claude Code with optional initial prompts.

async def connect(
    self, prompt: str | AsyncIterable[dict[str, Any]] | None = None
) -> None:
    """
    Connect to Claude with a prompt or message stream.

    Args:
        prompt: Optional initial prompt. Can be string, async iterable of messages, or None
                for empty connection that stays open for interactive use
    """

async def disconnect(self) -> None:
    """Disconnect from Claude."""

Message Communication

Send and receive messages with full bidirectional control.

async def receive_messages(self) -> AsyncIterator[Message]:
    """
    Receive all messages from Claude.

    Yields:
        Message: All messages in the conversation stream
    """

async def query(
    self, prompt: str | AsyncIterable[dict[str, Any]], session_id: str = "default"
) -> None:
    """
    Send a new request in streaming mode.

    Args:
        prompt: Either a string message or an async iterable of message dictionaries
        session_id: Session identifier for the conversation
    """

async def receive_response(self) -> AsyncIterator[Message]:
    """
    Receive messages from Claude until and including a ResultMessage.

    This async iterator yields all messages in sequence and automatically terminates
    after yielding a ResultMessage (which indicates the response is complete).

    Yields:
        Message: Each message received (UserMessage, AssistantMessage, SystemMessage, ResultMessage)
    """

Control Operations

Interrupt conversations and retrieve server information.

async def interrupt(self) -> None:
    """Send interrupt signal (only works with streaming mode)."""

async def get_server_info(self) -> dict[str, Any] | None:
    """
    Get server initialization info including available commands and output styles.

    Returns:
        Dictionary with server info, or None if not in streaming mode
    """

Context Management

Support for async context manager pattern.

async def __aenter__(self) -> "ClaudeSDKClient":
    """Enter async context - automatically connects with empty stream for interactive use."""

async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> bool:
    """Exit async context - always disconnects."""

Usage Examples

Basic Interactive Session

from claude_code_sdk import ClaudeSDKClient, AssistantMessage, TextBlock

async def main():
    async with ClaudeSDKClient() as client:
        # Send initial query
        await client.query("Hello, how are you?")

        # Receive and process response
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"Claude: {block.text}")

        # Send follow-up based on response
        await client.query("Can you help me write a Python function?")

        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for block in msg.content:
                    if isinstance(block, TextBlock):
                        print(f"Claude: {block.text}")

anyio.run(main)

Configuration with Options

from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions

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

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Create a Python web server")

        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Custom Tools Integration

from claude_code_sdk import (
    ClaudeSDKClient, ClaudeCodeOptions,
    tool, create_sdk_mcp_server
)

@tool("greet", "Greet a user", {"name": str})
async def greet_user(args):
    return {
        "content": [
            {"type": "text", "text": f"Hello, {args['name']}!"}
        ]
    }

async def main():
    # Create SDK MCP server
    server = create_sdk_mcp_server(
        name="my-tools",
        version="1.0.0",
        tools=[greet_user]
    )

    options = ClaudeCodeOptions(
        mcp_servers={"tools": server},
        allowed_tools=["mcp__tools__greet"]
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Greet Alice")

        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Hook System Integration

from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions, HookMatcher

async def check_bash_command(input_data, tool_use_id, context):
    tool_name = input_data["tool_name"]
    tool_input = input_data["tool_input"]

    if tool_name != "Bash":
        return {}

    command = tool_input.get("command", "")
    forbidden_patterns = ["rm -rf", "format"]

    for pattern in forbidden_patterns:
        if pattern in command:
            return {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"Command contains dangerous pattern: {pattern}",
                }
            }
    return {}

async def main():
    options = ClaudeCodeOptions(
        allowed_tools=["Bash"],
        hooks={
            "PreToolUse": [
                HookMatcher(matcher="Bash", hooks=[check_bash_command]),
            ],
        }
    )

    async with ClaudeSDKClient(options=options) as client:
        await client.query("Run the bash command: echo 'Hello World!'")

        async for msg in client.receive_response():
            print(msg)

anyio.run(main)

Interrupt Capability

import asyncio
from claude_code_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        await client.query("Write a very long story about space exploration")

        # Start receiving messages in background
        async def receive_messages():
            async for msg in client.receive_messages():
                print(msg)

        receive_task = asyncio.create_task(receive_messages())

        # Wait a bit, then interrupt
        await asyncio.sleep(5)
        await client.interrupt()

        await receive_task

anyio.run(main)

Server Information

from claude_code_sdk import ClaudeSDKClient

async def main():
    async with ClaudeSDKClient() as client:
        info = await client.get_server_info()

        if info:
            print(f"Commands available: {len(info.get('commands', []))}")
            print(f"Output style: {info.get('output_style', 'default')}")

anyio.run(main)

Manual Connection Management

from claude_code_sdk import ClaudeSDKClient

async def main():
    client = ClaudeSDKClient()

    try:
        await client.connect("Hello Claude")

        async for msg in client.receive_messages():
            print(msg)
            # Break after first complete response
            if hasattr(msg, 'subtype') and msg.subtype == "result":
                break

    finally:
        await client.disconnect()

anyio.run(main)

When to Use ClaudeSDKClient

Ideal for:

  • Building chat interfaces or conversational UIs
  • Interactive debugging or exploration sessions
  • Multi-turn conversations with context
  • When you need to react to Claude's responses
  • Real-time applications with user input
  • When you need interrupt capabilities
  • Using custom tools and hooks
  • Applications requiring advanced MCP features

Key Advantages over query():

  • Bidirectional communication
  • Stateful conversations
  • Interrupt support
  • Custom tool integration
  • Hook system support
  • Server information access
  • Fine-grained control over message flow

Important Limitations

Runtime Context: As of v0.0.20, you cannot use a ClaudeSDKClient instance across different async runtime contexts (e.g., different trio nurseries or asyncio task groups). The client maintains a persistent anyio task group that remains active from connect() until disconnect(), so all operations must be completed within the same async context where it was connected.

Error Handling

All ClaudeSDKClient methods can raise various exceptions:

  • CLIConnectionError: When not connected or connection issues occur
  • CLINotFoundError: When Claude Code is not installed
  • ProcessError: When the underlying CLI process fails
  • CLIJSONDecodeError: When response parsing fails

See Error Handling for complete error handling information.

Install with Tessl CLI

npx tessl i tessl/pypi-claude-code-sdk

docs

configuration-options.md

custom-tools.md

error-handling.md

hook-system.md

index.md

interactive-client.md

message-types.md

simple-queries.md

transport-system.md

tile.json