Python SDK for Claude Code with WebSocket server and multi-agent system support. See the Claude Code SDK documentation for more information.
- OAuth Authentication: Claude Code Max users can authenticate without API keys
- WebSocket Server: Real-time bidirectional communication with streaming support
- Agent System Integration: Build multi-agent applications with specialized AI agents
- Multi-Turn Conversations: Maintain context across multiple interactions with session management
- Tool Management API: Discover, create, and execute tools from a registry
- Enhanced Error Handling: Comprehensive error types and recovery mechanisms
- Type Safety: Full type hints and dataclass-based message types
- Jupyter Notebook Support: Beautiful markdown rendering and interactive displays
# Basic installation
pip install claude-code-sdk
# With WebSocket server support
pip install claude-code-sdk[websocket]
# With agent system (separate package)
pip install claude-code-agent-systemPrerequisites:
- Python 3.10+
- Node.js
- Claude Code:
npm install -g @anthropic-ai/claude-code
import anyio
from claude_code_sdk import query
async def main():
async for message in query(prompt="What is 2 + 2?"):
print(message)
anyio.run(main)Claude Code Max plan users can authenticate using OAuth without API keys:
# Login via CLI
claude-auth login
# Check authentication status
claude-auth statusUse in Python:
from claude_code_sdk import query_with_oauth
async for message in query_with_oauth(prompt="Hello Claude!"):
print(message)Traditional API key authentication:
export ANTHROPIC_API_KEY="your-api-key"from claude_code_sdk import query
async for message in query(prompt="Hello Claude!"):
print(message)See the Authentication Guide for more details.
from claude_code_sdk import query, ClaudeCodeOptions, AssistantMessage, TextBlock
# Simple query
async for message in query(prompt="Hello Claude"):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
print(block.text)
# With options
options = ClaudeCodeOptions(
system_prompt="You are a helpful assistant",
max_turns=1
)
async for message in query(prompt="Tell me a joke", options=options):
print(message)options = ClaudeCodeOptions(
allowed_tools=["Read", "Write", "Bash"],
permission_mode='acceptEdits' # auto-accept file edits
)
async for message in query(
prompt="Create a hello.py file",
options=options
):
# Process tool use and results
pass# Method 1: Using session IDs
session_id = None
async for message in query(prompt="Remember my name is Alice"):
if isinstance(message, ResultMessage):
session_id = message.session_id
# Continue the conversation
async for message in query(
prompt="What's my name?",
options=ClaudeCodeOptions(resume=session_id)
):
print(message)
# Method 2: Auto-continue last conversation
async for message in query(
prompt="What did we just discuss?",
options=ClaudeCodeOptions(continue_conversation=True)
):
print(message)See the Multi-Turn Conversations Guide for detailed examples.
from claude_code_sdk.conversation_manager import ConversationManager
from claude_code_sdk.conversation_templates import TemplateManager
from claude_code_sdk.conversation_chains import create_debugging_chain
# Managed conversations with persistence
manager = ConversationManager()
session_id, messages = await manager.create_conversation(
initial_prompt="Let's build a web app",
tags=["project", "web"]
)
# Use pre-built templates
template_mgr = TemplateManager()
template = template_mgr.get_template("code_review")
# Automated workflows with chains
debug_chain = create_debugging_chain()
result = await debug_chain.execute(
context_overrides={"issue_description": "Memory leak"}
)See the Advanced Conversation Features Guide for comprehensive documentation.
options = ClaudeCodeOptions(
# Add additional directories for tool access
add_dirs=["/path/to/libs", "/path/to/data"],
# Skip all permission prompts (use with caution!)
dangerously_skip_permissions=True,
# Enable debug logging in CLI
debug=True,
# Control output verbosity (default: True for SDK)
verbose=False,
# Specify output format (default: "stream-json")
output_format="json", # "text", "json", or "stream-json"
# Specify input format
input_format="text", # "text" or "stream-json"
)from pathlib import Path
options = ClaudeCodeOptions(
cwd="/path/to/project" # or Path("/path/to/project")
)Main async function for querying Claude.
Parameters:
prompt(str): The prompt to send to Claudeoptions(ClaudeCodeOptions): Optional configuration
Returns: AsyncIterator[Message] - Stream of response messages
The SDK provides wrapper functions for Claude Code CLI commands:
Check for and install updates to Claude Code.
result = await update()
print(result)Configure and manage Model Context Protocol (MCP) servers.
# List MCP servers
servers = await mcp(["list"])
# Add MCP server
result = await mcp(["add", "my-server"])
# Remove MCP server
result = await mcp(["remove", "my-server"])Manage Claude Code configuration.
# Get configuration value
model = await config(["get", "model"])
# Set configuration value
result = await config(["set", "model", "claude-opus-4"])
# List all config
all_config = await config(["list"])Check health of Claude Code auto-updater.
health = await doctor()
print(health)Get Claude Code version.
ver = await version()
print(f"Claude Code version: {ver}")See src/claude_code_sdk/types.py for complete type definitions:
ClaudeCodeOptions- Configuration optionsAssistantMessage,UserMessage,SystemMessage,ResultMessage- Message typesTextBlock,ToolUseBlock,ToolResultBlock- Content blocks
from claude_code_sdk import (
ClaudeSDKError, # Base error
CLINotFoundError, # Claude Code not installed
CLIConnectionError, # Connection issues
ProcessError, # Process failed
CLIJSONDecodeError, # JSON parsing issues
)
try:
async for message in query(prompt="Hello"):
pass
except CLINotFoundError:
print("Please install Claude Code")
except ProcessError as e:
print(f"Process failed with exit code: {e.exit_code}")
except CLIJSONDecodeError as e:
print(f"Failed to parse response: {e}")See src/claude_code_sdk/_errors.py for all error types.
Problem: The SDK cannot find the Claude Code CLI.
Solutions:
# Install Claude Code globally
npm install -g @anthropic-ai/claude-code
# Verify installation
claude --versionIf Node.js is not installed:
- macOS:
brew install nodeor download from nodejs.org - Linux: Use your package manager (e.g.,
sudo apt install nodejs npm) - Windows: Download installer from nodejs.org
Problem: The Claude Code process failed to start or crashed.
Common causes and solutions:
-
Missing API key: Set your Anthropic API key:
export ANTHROPIC_API_KEY="your-api-key"
-
Invalid configuration: Check your
ClaudeCodeOptionsfor typos or invalid values -
Permission issues: Ensure you have read/write permissions in the working directory
Debug steps:
try:
async for message in query(prompt="test"):
pass
except ProcessError as e:
print(f"Exit code: {e.exit_code}")
print(f"Error details: {e.stderr}")Problem: Cannot establish connection to Claude Code CLI.
Solutions:
- Check if another instance is already running
- Ensure sufficient system resources (RAM, disk space)
- Try with a simple prompt first to isolate the issue
Problem: Received malformed JSON from the CLI.
Common causes:
- Version mismatch between SDK and CLI
- Corrupted installation
Solutions:
# Update both SDK and CLI
pip install --upgrade claude-code-sdk
npm update -g @anthropic-ai/claude-codeProblem: Operations taking too long and timing out.
Solution: Wrap your code with custom timeout:
import asyncio
async def long_operation():
async with asyncio.timeout(300): # 5 minutes
async for message in query(prompt="Complex task..."):
# Process messages
passProblem: Cannot read/write files due to permissions.
Solutions:
- Run with appropriate user permissions
- Set working directory to a writable location:
options = ClaudeCodeOptions(cwd="/path/to/writable/directory")
- Use
permission_mode='bypassPermissions'(use with caution)
Problem: Too many requests to the API.
Solutions:
- Implement exponential backoff:
import asyncio async def query_with_retry(prompt, max_retries=3): for attempt in range(max_retries): try: async for message in query(prompt=prompt): yield message break except ProcessError as e: if "rate limit" in str(e).lower() and attempt < max_retries - 1: await asyncio.sleep(2 ** attempt) else: raise
Problem: Cannot connect to WebSocket server.
Common issues:
- Port already in use
- Firewall blocking connections
- CORS issues in browser
Solutions:
# Use a different port
server.run(host="0.0.0.0", port=8080)
# Check if port is in use
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex(('localhost', 8000))
if result == 0:
print("Port 8000 is already in use")import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
# The SDK will now log:
# - CLI discovery process
# - Connection attempts
# - Message parsing
# - Error detailsimport claude_code_sdk
print(f"SDK version: {claude_code_sdk.__version__}")claude --versionasync for message in query(prompt="Debug test"):
print(f"Message type: {type(message).__name__}")
print(f"Message content: {message}")
if hasattr(message, 'content'):
for block in message.content:
print(f" Block type: {type(block).__name__}")# Start with the simplest possible query
async for message in query(prompt="Hello"):
print(message)
# If that works, gradually add complexity
options = ClaudeCodeOptions(allowed_tools=["Read"])
async for message in query(prompt="Read README.md", options=options):
print(message)If you're still experiencing issues:
- Check the Claude Code documentation
- Search existing GitHub issues
- Create a new issue with:
- Python version (
python --version) - SDK version (
pip show claude-code-sdk) - CLI version (
claude --version) - Minimal code to reproduce the issue
- Full error message and stack trace
- Python version (
Always use async context managers or ensure proper cleanup:
# Good: Automatic cleanup
async def process_files():
async for message in query(prompt="Process files"):
# SDK handles cleanup automatically
pass
# For long-running applications
import asyncio
async def main():
try:
async for message in query(prompt="Long task"):
process_message(message)
except KeyboardInterrupt:
# Graceful shutdown
print("Shutting down...")Implement comprehensive error handling:
from claude_code_sdk import (
query,
CLINotFoundError,
ProcessError,
CLIConnectionError
)
async def safe_query(prompt: str, max_retries: int = 3):
"""Query with automatic retry and error handling."""
for attempt in range(max_retries):
try:
async for message in query(prompt=prompt):
yield message
break
except CLINotFoundError:
# Can't recover from this
raise
except (CLIConnectionError, ProcessError) as e:
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt)
continue
raiseBe specific with tool permissions:
# Good: Only request needed tools
options = ClaudeCodeOptions(
allowed_tools=["Read", "Write"], # Specific tools
disallowed_tools=["Bash"], # Explicitly disallow
)
# Better: Use appropriate permission mode
options = ClaudeCodeOptions(
allowed_tools=["Read", "Write", "Edit"],
permission_mode="acceptEdits", # Auto-accept safe operations
)
# Avoid: Too permissive
# options = ClaudeCodeOptions(permission_mode="bypassPermissions")Process messages efficiently:
from claude_code_sdk import AssistantMessage, TextBlock, ToolUseBlock
async def process_claude_response(prompt: str):
"""Process different message types appropriately."""
tool_uses = []
text_responses = []
async for message in query(prompt=prompt):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
text_responses.append(block.text)
elif isinstance(block, ToolUseBlock):
tool_uses.append({
"tool": block.name,
"input": block.input
})
return {
"text": "\n".join(text_responses),
"tools_used": tool_uses
}For multi-turn conversations:
# Continue previous session
options = ClaudeCodeOptions(continue_conversation=True)
# Or save and resume specific sessions
session_id = None
async def chat(user_input: str):
global session_id
options = ClaudeCodeOptions(
resume=session_id if session_id else None,
max_turns=10 # Prevent runaway conversations
)
async for message in query(prompt=user_input, options=options):
if isinstance(message, ResultMessage):
session_id = message.session_id
yield messageAlways set explicit working directories:
from pathlib import Path
# Good: Explicit, absolute path
options = ClaudeCodeOptions(
cwd=Path("/home/user/projects/myapp").absolute()
)
# Good: Relative to script location
script_dir = Path(__file__).parent
options = ClaudeCodeOptions(
cwd=script_dir / "workspace"
)
# Avoid: Implicit current directory
# options = ClaudeCodeOptions() # Uses wherever script is run fromFor better performance:
# Batch operations when possible
prompt = """
Please perform these tasks:
1. Read all Python files in the src/ directory
2. Analyze the code structure
3. Generate a summary report
"""
# Instead of multiple queries
# DON'T: Multiple round trips
# for file in files:
# async for msg in query(f"Read {file}"):
# ...
# DO: Single comprehensive request
async for message in query(prompt=prompt, options=options):
# Process all results at once
passImplement proper logging:
import logging
from claude_code_sdk import ResultMessage
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
async def monitored_query(prompt: str):
"""Query with monitoring."""
start_time = time.time()
try:
async for message in query(prompt=prompt):
if isinstance(message, ResultMessage):
logger.info(
f"Query completed - "
f"Cost: ${message.cost_usd:.4f}, "
f"Duration: {message.duration_ms}ms, "
f"Session: {message.session_id}"
)
yield message
except Exception as e:
logger.error(f"Query failed: {e}", exc_info=True)
raise
finally:
duration = time.time() - start_time
logger.info(f"Total duration: {duration:.2f}s")Never expose sensitive information:
import os
# Good: Use environment variables
options = ClaudeCodeOptions(
system_prompt="Use the API key from environment"
)
# Set via environment
os.environ["ANTHROPIC_API_KEY"] = "your-key"
# Avoid: Hardcoding secrets
# DON'T: options = ClaudeCodeOptions(
# system_prompt="Use API key: sk-ant-..."
# )
# Good: Sanitize file paths
safe_path = Path(user_input).resolve()
if not safe_path.is_relative_to(allowed_directory):
raise ValueError("Access denied")Write testable code:
# Make your code testable
async def analyze_code(
file_path: str,
query_func=query # Injectable for testing
):
"""Analyze code with injectable query function."""
prompt = f"Analyze the code in {file_path}"
async for message in query_func(prompt=prompt):
# Process message
pass
# In tests
async def mock_query(prompt, options=None):
"""Mock query for testing."""
yield AssistantMessage(content=[
TextBlock(text="Mock analysis complete")
])
# Test
async def test_analyze():
await analyze_code("test.py", query_func=mock_query)For operations that might take a long time:
import asyncio
from contextlib import asynccontextmanager
@asynccontextmanager
async def timeout_query(prompt: str, timeout_seconds: int = 300):
"""Query with timeout and cleanup."""
task = None
try:
async with asyncio.timeout(timeout_seconds):
task = asyncio.create_task(collect_messages(prompt))
yield task
except asyncio.TimeoutError:
logger.warning(f"Query timed out after {timeout_seconds}s")
raise
finally:
if task and not task.done():
task.cancel()
async def collect_messages(prompt: str):
"""Collect all messages from a query."""
messages = []
async for message in query(prompt=prompt):
messages.append(message)
return messages-
Don't use synchronous code in async context:
# Bad time.sleep(1) # Blocks event loop # Good await asyncio.sleep(1)
-
Don't ignore error messages:
# Bad try: async for msg in query(prompt="..."): pass except Exception: pass # Silent failure # Good except Exception as e: logger.error(f"Query failed: {e}") raise
-
Don't leak resources:
# Bad messages = [] async for msg in query(prompt="..."): messages.append(msg) if len(messages) > 1000000: # Memory leak break # Good async for msg in query(prompt="..."): process_message(msg) # Process and discard
See the Claude Code documentation for a complete list of available tools.
The SDK includes a WebSocket server for real-time communication:
from claude_code_sdk.websocket_server import EnhancedClaudeWebSocketServer
server = EnhancedClaudeWebSocketServer()
server.run(host="0.0.0.0", port=8000)Connect from JavaScript:
const ws = new WebSocket('ws://localhost:8000/ws');
ws.send(JSON.stringify({
type: 'query',
prompt: 'Hello Claude!',
options: { allowed_tools: ['Read', 'Write'] }
}));See docs/websocket-server.md for full documentation.
Build multi-agent applications using the separate agent system package:
from claude_code_sdk import query, ClaudeCodeOptions
from claude_code_agent_system import BaseAgent
class CustomAgent(BaseAgent):
async def process_with_claude(self, prompt: str):
options = ClaudeCodeOptions(allowed_tools=["Read", "Write"])
async for message in query(prompt=prompt, options=options):
# Process responses
passSee docs/agent-system.md for integration guide.
The SDK includes special utilities for enhanced display in Jupyter notebooks:
from claude_code_sdk.notebook_utils import display_claude_response, render_markdown
# Beautiful markdown rendering
async for message in query(prompt="Explain Python decorators"):
display_claude_response(message)
# Or render markdown directly
render_markdown("# Hello\nThis is **bold** text with `code`")Features:
- Markdown to HTML conversion with syntax highlighting
- Tool use visualization with colored formatting
- Streaming support for real-time display
- Automatic notebook detection
See docs/notebook_utilities.md for full documentation and examples/notebook_markdown_demo.ipynb for a complete demo.
- examples/quick_start.py - Basic SDK usage
- examples/websocket_ui_server.py - WebSocket server with UI
- examples/agent_sdk_integration.py - Agent system integration
- agent_system/ - Complete agent system implementation
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
MIT