Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:00:18 +08:00
commit 765529cd13
69 changed files with 18291 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
# Agent and Subagent Definition Patterns
This guide covers how to define agents and subagents programmatically using the Claude Agent SDK.
## Core Concepts
**AgentDefinition** - Defines a specialized agent with specific tools, prompts, and model configuration.
**Programmatic Definition** - SDK best practice is to define agents programmatically using the `agents` parameter in `ClaudeAgentOptions`, not filesystem auto-discovery.
## Basic Agent Definition
```python
from claude_agent_sdk import AgentDefinition, ClaudeAgentOptions
options = ClaudeAgentOptions(
agents={
"agent-name": AgentDefinition(
description="When to use this agent",
prompt="System prompt defining role and behavior",
tools=["Read", "Grep", "Glob"],
model="sonnet" # or "opus", "haiku", "inherit"
)
}
)
```
## AgentDefinition Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `description` | `str` | Yes | Natural language description of when to use this agent |
| `prompt` | `str` | Yes | Agent's system prompt defining role and behavior |
| `tools` | `list[str]` | No | Array of allowed tool names. If omitted, inherits all tools |
| `model` | `str` | No | Model override: "sonnet", "opus", "haiku", or "inherit" |
## Common Patterns
### Read-Only Analyzer Agent
For code review, architecture analysis, or documentation review:
```python
"code-reviewer": AgentDefinition(
description="Reviews code for best practices, security, and performance",
prompt="""You are a code reviewer. Analyze code for:
- Security vulnerabilities
- Performance issues
- Best practice adherence
- Potential bugs
Provide constructive, specific feedback.""",
tools=["Read", "Grep", "Glob"],
model="sonnet"
)
```
### Code Modification Agent
For implementing features or fixing bugs:
```python
"code-writer": AgentDefinition(
description="Implements features and fixes bugs",
prompt="""You are a code implementation specialist.
Write clean, tested, well-documented code.
Follow project conventions and best practices.""",
tools=["Read", "Write", "Edit", "Grep", "Glob"],
model="sonnet"
)
```
### Test Execution Agent
For running tests and analyzing results:
```python
"test-runner": AgentDefinition(
description="Runs tests and analyzes results",
prompt="""You are a testing specialist.
Execute tests, analyze failures, and provide clear diagnostics.
Focus on actionable feedback.""",
tools=["Bash", "Read", "Grep"],
model="sonnet"
)
```
### Multiple Agents Pattern
Orchestrator with specialized subagents:
```python
options = ClaudeAgentOptions(
system_prompt="claude_code", # Orchestrator needs Task tool knowledge
allowed_tools=["Bash", "Task", "Read", "Write"],
agents={
"analyzer": AgentDefinition(
description="Analyzes code structure and patterns",
prompt="You are a code analyzer. Examine structure, patterns, and architecture.",
tools=["Read", "Grep", "Glob"]
),
"fixer": AgentDefinition(
description="Fixes identified issues",
prompt="You are a code fixer. Apply fixes based on analysis results.",
tools=["Read", "Edit", "Bash"],
model="sonnet"
)
}
)
```
## Loading Agent Definitions from Files
While programmatic definition is recommended, you can still store agent prompts in markdown files:
```python
import yaml
def load_agent_definition(path: str) -> AgentDefinition:
"""Load agent definition from markdown file with YAML frontmatter."""
with open(path) as f:
content = f.read()
parts = content.split("---")
frontmatter = yaml.safe_load(parts[1])
system_prompt = parts[2].strip()
# Parse tools (can be comma-separated string or array)
tools_value = frontmatter.get("tools", [])
if isinstance(tools_value, str):
tools = [t.strip() for t in tools_value.split(",")]
else:
tools = tools_value
return AgentDefinition(
description=frontmatter["description"],
prompt=system_prompt,
tools=tools,
model=frontmatter.get("model", "inherit")
)
# Load and register programmatically
investigator = load_agent_definition(".claude/agents/investigator.md")
options = ClaudeAgentOptions(
agents={"investigator": investigator}
)
```
## Best Practices
1. **Use programmatic registration** - Define agents via `agents` parameter, not filesystem auto-discovery
2. **Set orchestrator system_prompt** - Main agent needs `system_prompt="claude_code"` to use Task tool
3. **Specific descriptions** - Agent descriptions determine when they're used
4. **Restrict tools** - Limit agent tools to minimum needed for safety and clarity
5. **Match agent names** - Ensure agent names in `agents={}` match what orchestrator references
## Anti-Patterns
**Relying on filesystem auto-discovery**
```python
# SDK will auto-discover .claude/agents/*.md but this is NOT recommended
options = ClaudeAgentOptions() # Missing explicit agents={}
```
**Programmatic registration**
```python
options = ClaudeAgentOptions(
agents={"agent-name": AgentDefinition(...)}
)
```
**Missing orchestrator system prompt**
```python
options = ClaudeAgentOptions(
agents={...}
# Missing system_prompt="claude_code"
)
```
**Proper orchestrator configuration**
```python
options = ClaudeAgentOptions(
system_prompt="claude_code", # Orchestrator knows how to use Task tool
agents={...}
)
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,406 @@
# Claude Agent SDK Best Practices
This guide captures best practices and common patterns for building effective SDK applications.
## Agent Definition
### ✅ Use Programmatic Registration
**Recommended:** Define agents via `agents` parameter
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
description="Analyzes errors autonomously",
prompt="You are an error investigator...",
tools=["Read", "Grep", "Glob", "Bash"]
)
}
)
```
**Not Recommended:** Relying on filesystem auto-discovery
```python
# SDK can auto-discover .claude/agents/*.md
# but programmatic registration is clearer and more maintainable
options = ClaudeAgentOptions()
```
### ✅ Set Orchestrator System Prompt
**Critical:** Orchestrators must use `system_prompt="claude_code"`
```python
options = ClaudeAgentOptions(
system_prompt="claude_code", # Knows how to use Task tool
allowed_tools=["Bash", "Task", "Read", "Write"],
agents={...}
)
```
**Why:** The claude_code preset includes knowledge of the Task tool for delegating to subagents.
### ✅ Match Agent Names
Ensure agent names in `agents={}` match references in prompts:
```python
# Define agent
options = ClaudeAgentOptions(
agents={"markdown-investigator": AgentDefinition(...)}
)
# Reference in prompt
await client.query("Use the 'markdown-investigator' subagent...")
```
## Tool Configuration
### ✅ Restrict Subagent Tools
Limit subagent tools to minimum needed:
```python
# Read-only analyzer
"analyzer": AgentDefinition(
tools=["Read", "Grep", "Glob"]
)
# Code modifier
"fixer": AgentDefinition(
tools=["Read", "Edit", "Bash"]
)
```
### ✅ Give Orchestrator Task Tool
Orchestrators need Task tool to delegate:
```python
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Task", "Read", "Write"], # Include Task
agents={...}
)
```
## Async/Await Patterns
### ✅ Use async with for Streaming
```python
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
# Process messages
pass
```
### ✅ Handle Multiple Message Types
```python
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
text = block.text
elif isinstance(message, ResultMessage):
print(f"Cost: ${message.total_cost_usd:.4f}")
print(f"Duration: {message.duration_ms}ms")
```
## Error Handling
### ✅ Validate Agent Responses
Don't assume agents return expected format:
```python
investigation_report = None
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
# Try to extract JSON
try:
investigation_report = json.loads(block.text)
except json.JSONDecodeError:
# Handle non-JSON response
continue
if not investigation_report:
raise RuntimeError("Agent did not return valid report")
```
### ✅ Use uv Script Headers
For standalone SDK scripts, use uv inline script metadata:
```python
#!/usr/bin/env -S uv run --script --quiet
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "claude-agent-sdk>=0.1.6",
# ]
# ///
```
## Project Structure
### ✅ Organize Agent Definitions
Option 1: Store in markdown files, load programmatically
```text
project/
├── .claude/
│ └── agents/
│ ├── investigator.md
│ └── fixer.md
├── main.py
```
```python
def load_agent_definition(path: str) -> AgentDefinition:
# Parse frontmatter and content
# Return AgentDefinition
investigator = load_agent_definition(".claude/agents/investigator.md")
options = ClaudeAgentOptions(agents={"investigator": investigator})
```
Option 2: Define inline
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
description="...",
prompt="...",
tools=[...]
)
}
)
```
## Permission Management
### ✅ Choose Appropriate Permission Mode
```python
# Automated workflows (auto-approve edits)
options = ClaudeAgentOptions(
permission_mode="acceptEdits"
)
# Interactive development (ask for approval)
options = ClaudeAgentOptions(
permission_mode="default",
can_use_tool=permission_callback
)
# Read-only mode (use tool restrictions)
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob"] # Only read tools
)
```
### ✅ Use Hooks for Complex Logic
Prefer hooks over permission callbacks for:
- Adding context
- Reviewing outputs
- Stopping on errors
```python
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_command])
],
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[review_output])
]
}
)
```
## Common Anti-Patterns
### ❌ Missing System Prompt on Orchestrator
```python
# Orchestrator won't know how to use Task tool
options = ClaudeAgentOptions(
agents={...}
# Missing system_prompt="claude_code"
)
```
### ❌ Tool/Prompt Mismatch
```python
# Tells agent to modify files but only allows read tools
options = ClaudeAgentOptions(
system_prompt="Fix any bugs you find",
allowed_tools=["Read", "Grep"] # Can't actually fix
)
```
### ❌ Assuming Agent Output Format
```python
# Assumes agent returns JSON
json_data = json.loads(message.content[0].text) # May crash
```
### ❌ Not Validating Agent Names
```python
# Define as "investigator" but reference as "markdown-investigator"
options = ClaudeAgentOptions(
agents={"investigator": AgentDefinition(...)}
)
await client.query("Use 'markdown-investigator'...") # Won't work
```
## Performance Optimization
### ✅ Use Appropriate Models
```python
# Fast, cheap tasks
"simple-agent": AgentDefinition(model="haiku", ...)
# Complex reasoning
"complex-agent": AgentDefinition(model="sonnet", ...)
# Inherit from main agent
"helper-agent": AgentDefinition(model="inherit", ...)
```
### ✅ Set Budget Limits
```python
options = ClaudeAgentOptions(
max_budget_usd=1.00 # Stop after $1
)
```
### ✅ Limit Turns for Simple Tasks
```python
options = ClaudeAgentOptions(
max_turns=3 # Prevent infinite loops
)
```
## Testing
### ✅ Validate Agent Definitions
```python
def test_agent_configuration():
"""Ensure agent definitions are valid."""
options = get_sdk_options()
# Check orchestrator has claude_code preset
# Note: Can be string "claude_code" or dict {"type": "preset", "preset": "claude_code"}
assert options.system_prompt in ("claude_code", {"type": "preset", "preset": "claude_code"})
# Check orchestrator has Task tool
assert "Task" in options.allowed_tools
# Check agents are registered
assert "investigator" in options.agents
assert "fixer" in options.agents
```
### ✅ Test Tool Restrictions
```python
def test_subagent_tools():
"""Ensure subagents have correct tools."""
options = get_sdk_options()
investigator = options.agents["investigator"]
assert "Read" in investigator.tools
assert "Write" not in investigator.tools # Read-only
```
## Documentation
### ✅ Document Agent Purposes
```python
options = ClaudeAgentOptions(
agents={
"investigator": AgentDefinition(
# Clear, specific description
description=(
"Autonomous analyzer that determines if markdown errors "
"are fixable or false positives"
),
prompt="...",
tools=[...]
)
}
)
```
### ✅ Document Workflow
```python
"""
Intelligent Markdown Linting Orchestrator
Architecture:
- Orchestrator (main): Strategic coordination
- Investigator subagent: Autonomous error analysis
- Fixer subagent: Execute fixes with context
Workflow:
1. Discovery: Run linter, parse output
2. Triage: Classify errors (simple vs ambiguous)
3. Investigation: Investigator analyzes ambiguous errors
4. Fixing: Fixer applies fixes based on investigation
5. Verification: Re-run linter to confirm fixes
"""
```
## Debugging
### ✅ Log Agent Communication
```python
all_response_text = []
async for message in client.receive_response():
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
all_response_text.append(block.text)
print(f"Agent: {block.text}")
# Save full transcript for debugging
with open("debug_transcript.txt", "w") as f:
f.write("\n\n".join(all_response_text))
```
### ✅ Track Costs
```python
async for message in client.receive_response():
if isinstance(message, ResultMessage):
if message.total_cost_usd:
print(f"Total cost: ${message.total_cost_usd:.4f}")
if message.duration_ms:
print(f"Duration: {message.duration_ms}ms")
```

View File

@@ -0,0 +1,715 @@
# Custom Tools
> Build and integrate custom tools to extend Claude Agent SDK functionality
Custom tools allow you to extend Claude Code's capabilities with your own
functionality through in-process MCP servers, enabling Claude to interact with
external services, APIs, or perform specialized operations.
## Creating Custom Tools
Use the `createSdkMcpServer` and `tool` helper functions to define type-safe custom tools:
```typescript
const customServer = createSdkMcpServer({
name: "my-custom-tools",
version: "1.0.0",
tools: [
tool(
"get_weather",
"Get current weather for a location",
{
location: z.string().describe("City name or coordinates"),
units: z.enum(["celsius", "fahrenheit"]).default("celsius").describe("Temperature units")
},
async (args) => {
// Call weather API
const response = await fetch(
`https://api.weather.com/v1/current?q=${args.location}&units=${args.units}`
);
const data = await response.json();
return {
content: [{
type: "text",
text: `Temperature: ${data.temp}°\nConditions: ${data.conditions}\nHumidity: ${data.humidity}%`
}]
};
}
)
]
});
```
```python
from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeSDKClient, ClaudeAgentOptions
from typing import Any
import aiohttp
# Define a custom tool using the @tool decorator
@tool("get_weather", "Get current weather for a location", {"location": str, "units": str})
async def get_weather(args: dict[str, Any]) -> dict[str, Any]:
# Call weather API
units = args.get('units', 'celsius')
async with aiohttp.ClientSession() as session:
async with session.get(
f"https://api.weather.com/v1/current?q={args['location']}&units={units}"
) as response:
data = await response.json()
return {
"content": [{
"type": "text",
"text": f"Temperature: {data['temp']}°\nConditions: {data['conditions']}\nHumidity: {data['humidity']}%"
}]
}
# Create an SDK MCP server with the custom tool
custom_server = create_sdk_mcp_server(
name="my-custom-tools",
version="1.0.0",
tools=[get_weather] # Pass the decorated function
)
```
## Using Custom Tools
Pass the custom server to the `query` function via the `mcpServers` option as a dictionary/object.
**Important:** Custom MCP tools require streaming input mode. You must use an
async generator/iterable for the `prompt` parameter - a simple string will not
work with MCP servers.
### Tool Name Format
When MCP tools are exposed to Claude, their names follow a specific format:
* Pattern: `mcp__{server_name}__{tool_name}`
* Example: A tool named `get_weather` in server `my-custom-tools` becomes `mcp__my-custom-tools__get_weather`
### Configuring Allowed Tools
You can control which tools Claude can use via the `allowedTools` option:
```typescript
async function* generateMessages() {
yield {
type: "user",
message: {
content: "What's the weather in San Francisco?"
}
};
}
for await (const message of query({
prompt: generateMessages(), // Use async generator for streaming input
options: {
mcpServers: {
"my-custom-tools": customServer // Pass as object/dictionary, not array
},
// Optionally specify which tools Claude can use
allowedTools: [
"mcp__my-custom-tools__get_weather", // Allow the weather tool
// Add other tools as needed
],
maxTurns: 3
}
})) {
if (message.type === "result" && message.subtype === "success") {
console.log(message.result);
}
}
```
```python
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
import asyncio
# Use the custom tools with Claude
options = ClaudeAgentOptions(
mcp_servers={"my-custom-tools": custom_server},
allowed_tools=[
"mcp__my-custom-tools__get_weather", # Allow the weather tool
# Add other tools as needed
]
)
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("What's the weather in San Francisco?")
# Extract and print response
async for msg in client.receive_response():
print(msg)
asyncio.run(main())
```
## Multiple Tools Example
When your MCP server has multiple tools, you can selectively allow them:
```typescript
const multiToolServer = createSdkMcpServer({
name: "utilities",
version: "1.0.0",
tools: [
tool("calculate", "Perform calculations", { /* ... */ }, async (args) => { /* ... */ }),
tool("translate", "Translate text", { /* ... */ }, async (args) => { /* ... */ }),
tool("search_web", "Search the web", { /* ... */ }, async (args) => { /* ... */ })
]
});
// Allow only specific tools with streaming input
async function* generateMessages() {
yield {
type: "user",
message: {
content: "Calculate 5 + 3 and translate 'hello' to Spanish"
}
};
}
for await (const message of query({
prompt: generateMessages(), // Use async generator for streaming input
options: {
mcpServers: {
utilities: multiToolServer
},
allowedTools: [
"mcp__utilities__calculate", // Allow calculator
"mcp__utilities__translate", // Allow translator
// "mcp__utilities__search_web" is NOT allowed
]
}
})) {
// Process messages
}
```
```python
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, tool, create_sdk_mcp_server
from typing import Any
import asyncio
# Define multiple tools using the @tool decorator
@tool("calculate", "Perform calculations", {"expression": str})
async def calculate(args: dict[str, Any]) -> dict[str, Any]:
result = eval(args["expression"]) # Use safe eval in production
return {"content": [{"type": "text", "text": f"Result: {result}"}]}
@tool("translate", "Translate text", {"text": str, "target_lang": str})
async def translate(args: dict[str, Any]) -> dict[str, Any]:
# Translation logic here
return {"content": [{"type": "text", "text": f"Translated: {args['text']}"}]}
@tool("search_web", "Search the web", {"query": str})
async def search_web(args: dict[str, Any]) -> dict[str, Any]:
# Search logic here
return {"content": [{"type": "text", "text": f"Search results for: {args['query']}"}]}
multi_tool_server = create_sdk_mcp_server(
name="utilities",
version="1.0.0",
tools=[calculate, translate, search_web] # Pass decorated functions
)
# Allow only specific tools with streaming input
async def message_generator():
yield {
"type": "user",
"message": {
"role": "user",
"content": "Calculate 5 + 3 and translate 'hello' to Spanish"
}
}
async for message in query(
prompt=message_generator(), # Use async generator for streaming input
options=ClaudeAgentOptions(
mcp_servers={"utilities": multi_tool_server},
allowed_tools=[
"mcp__utilities__calculate", # Allow calculator
"mcp__utilities__translate", # Allow translator
# "mcp__utilities__search_web" is NOT allowed
]
)
):
if hasattr(message, 'result'):
print(message.result)
```
## Type Safety with Python
The `@tool` decorator supports various schema definition approaches for type safety:
```typescript
tool(
"process_data",
"Process structured data with type safety",
{
// Zod schema defines both runtime validation and TypeScript types
data: z.object({
name: z.string(),
age: z.number().min(0).max(150),
email: z.string().email(),
preferences: z.array(z.string()).optional()
}),
format: z.enum(["json", "csv", "xml"]).default("json")
},
async (args) => {
// args is fully typed based on the schema
// TypeScript knows: args.data.name is string, args.data.age is number, etc.
console.log(`Processing ${args.data.name}'s data as ${args.format}`);
// Your processing logic here
return {
content: [{
type: "text",
text: `Processed data for ${args.data.name}`
}]
};
}
)
```
```python
from typing import Any
# Simple type mapping - recommended for most cases
@tool(
"process_data",
"Process structured data with type safety",
{
"name": str,
"age": int,
"email": str,
"preferences": list # Optional parameters can be handled in the function
}
)
async def process_data(args: dict[str, Any]) -> dict[str, Any]:
# Access arguments with type hints for IDE support
name = args["name"]
age = args["age"]
email = args["email"]
preferences = args.get("preferences", [])
print(f"Processing {name}'s data (age: {age})")
return {
"content": [{
"type": "text",
"text": f"Processed data for {name}"
}]
}
# For more complex schemas, you can use JSON Schema format
@tool(
"advanced_process",
"Process data with advanced validation",
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"email": {"type": "string", "format": "email"},
"format": {"type": "string", "enum": ["json", "csv", "xml"], "default": "json"}
},
"required": ["name", "age", "email"]
}
)
async def advanced_process(args: dict[str, Any]) -> dict[str, Any]:
# Process with advanced schema validation
return {
"content": [{
"type": "text",
"text": f"Advanced processing for {args['name']}"
}]
}
```
## Error Handling
Handle errors gracefully to provide meaningful feedback:
```typescript
tool(
"fetch_data",
"Fetch data from an API",
{
endpoint: z.string().url().describe("API endpoint URL")
},
async (args) => {
try {
const response = await fetch(args.endpoint);
if (!response.ok) {
return {
content: [{
type: "text",
text: `API error: ${response.status} ${response.statusText}`
}]
};
}
const data = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Failed to fetch data: ${error.message}`
}]
};
}
}
)
```
```python
import json
import aiohttp
from typing import Any
@tool(
"fetch_data",
"Fetch data from an API",
{"endpoint": str} # Simple schema
)
async def fetch_data(args: dict[str, Any]) -> dict[str, Any]:
try:
async with aiohttp.ClientSession() as session:
async with session.get(args["endpoint"]) as response:
if response.status != 200:
return {
"content": [{
"type": "text",
"text": f"API error: {response.status} {response.reason}"
}]
}
data = await response.json()
return {
"content": [{
"type": "text",
"text": json.dumps(data, indent=2)
}]
}
except Exception as e:
return {
"content": [{
"type": "text",
"text": f"Failed to fetch data: {str(e)}"
}]
}
```
## Example Tools
### Database Query Tool
```typescript
const databaseServer = createSdkMcpServer({
name: "database-tools",
version: "1.0.0",
tools: [
tool(
"query_database",
"Execute a database query",
{
query: z.string().describe("SQL query to execute"),
params: z.array(z.any()).optional().describe("Query parameters")
},
async (args) => {
const results = await db.query(args.query, args.params || []);
return {
content: [{
type: "text",
text: `Found ${results.length} rows:\n${JSON.stringify(results, null, 2)}`
}]
};
}
)
]
});
```
```python
from typing import Any
import json
@tool(
"query_database",
"Execute a database query",
{"query": str, "params": list} # Simple schema with list type
)
async def query_database(args: dict[str, Any]) -> dict[str, Any]:
results = await db.query(args["query"], args.get("params", []))
return {
"content": [{
"type": "text",
"text": f"Found {len(results)} rows:\n{json.dumps(results, indent=2)}"
}]
}
database_server = create_sdk_mcp_server(
name="database-tools",
version="1.0.0",
tools=[query_database] # Pass the decorated function
)
```
### API Gateway Tool
```typescript
const apiGatewayServer = createSdkMcpServer({
name: "api-gateway",
version: "1.0.0",
tools: [
tool(
"api_request",
"Make authenticated API requests to external services",
{
service: z.enum(["stripe", "github", "openai", "slack"]).describe("Service to call"),
endpoint: z.string().describe("API endpoint path"),
method: z.enum(["GET", "POST", "PUT", "DELETE"]).describe("HTTP method"),
body: z.record(z.any()).optional().describe("Request body"),
query: z.record(z.string()).optional().describe("Query parameters")
},
async (args) => {
const config = {
stripe: { baseUrl: "<https://api.stripe.com/v1>", key: process.env.STRIPE_KEY },
github: { baseUrl: "<https://api.github.com>", key: process.env.GITHUB_TOKEN },
openai: { baseUrl: "<https://api.openai.com/v1>", key: process.env.OPENAI_KEY },
slack: { baseUrl: "<https://slack.com/api>", key: process.env.SLACK_TOKEN }
};
const { baseUrl, key } = config[args.service];
const url = new URL(`${baseUrl}${args.endpoint}`);
if (args.query) {
Object.entries(args.query).forEach(([k, v]) => url.searchParams.set(k, v));
}
const response = await fetch(url, {
method: args.method,
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: args.body ? JSON.stringify(args.body) : undefined
});
const data = await response.json();
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2)
}]
};
}
)
]
});
```
```python
import os
import json
import aiohttp
from typing import Any
# For complex schemas with enums, use JSON Schema format
@tool(
"api_request",
"Make authenticated API requests to external services",
{
"type": "object",
"properties": {
"service": {"type": "string", "enum": ["stripe", "github", "openai", "slack"]},
"endpoint": {"type": "string"},
"method": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE"]},
"body": {"type": "object"},
"query": {"type": "object"}
},
"required": ["service", "endpoint", "method"]
}
)
async def api_request(args: dict[str, Any]) -> dict[str, Any]:
config = {
"stripe": {"base_url": "<https://api.stripe.com/v1>", "key": os.environ["STRIPE_KEY"]},
"github": {"base_url": "<https://api.github.com>", "key": os.environ["GITHUB_TOKEN"]},
"openai": {"base_url": "<https://api.openai.com/v1>", "key": os.environ["OPENAI_KEY"]},
"slack": {"base_url": "<https://slack.com/api>", "key": os.environ["SLACK_TOKEN"]}
}
service_config = config[args["service"]]
url = f"{service_config['base_url']}{args['endpoint']}"
if args.get("query"):
params = "&".join([f"{k}={v}" for k, v in args["query"].items()])
url += f"?{params}"
headers = {"Authorization": f"Bearer {service_config['key']}", "Content-Type": "application/json"}
async with aiohttp.ClientSession() as session:
async with session.request(
args["method"], url, headers=headers, json=args.get("body")
) as response:
data = await response.json()
return {
"content": [{
"type": "text",
"text": json.dumps(data, indent=2)
}]
}
api_gateway_server = create_sdk_mcp_server(
name="api-gateway",
version="1.0.0",
tools=[api_request] # Pass the decorated function
)
```
## Calculator Tool
```typescript
const calculatorServer = createSdkMcpServer({
name: "calculator",
version: "1.0.0",
tools: [
tool(
"calculate",
"Perform mathematical calculations",
{
expression: z.string().describe("Mathematical expression to evaluate"),
precision: z.number().optional().default(2).describe("Decimal precision")
},
async (args) => {
try {
// Use a safe math evaluation library in production
const result = eval(args.expression); // Example only!
const formatted = Number(result).toFixed(args.precision);
return {
content: [{
type: "text",
text: `${args.expression} = ${formatted}`
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error: Invalid expression - ${error.message}`
}]
};
}
}
),
tool(
"compound_interest",
"Calculate compound interest for an investment",
{
principal: z.number().positive().describe("Initial investment amount"),
rate: z.number().describe("Annual interest rate (as decimal, e.g., 0.05 for 5%)"),
time: z.number().positive().describe("Investment period in years"),
n: z.number().positive().default(12).describe("Compounding frequency per year")
},
async (args) => {
const amount = args.principal * Math.pow(1 + args.rate / args.n, args.n * args.time);
const interest = amount - args.principal;
return {
content: [{
type: "text",
text: `Investment Analysis:\n` +
`Principal: $${args.principal.toFixed(2)}\n` +
`Rate: ${(args.rate * 100).toFixed(2)}%\n` +
`Time: ${args.time} years\n` +
`Compounding: ${args.n} times per year\n\n` +
`Final Amount: $${amount.toFixed(2)}\n` +
`Interest Earned: $${interest.toFixed(2)}\n` +
`Return: ${((interest / args.principal) * 100).toFixed(2)}%`
}]
};
}
)
]
});
```
```python
import math
from typing import Any
@tool(
"calculate",
"Perform mathematical calculations",
{"expression": str, "precision": int} # Simple schema
)
async def calculate(args: dict[str, Any]) -> dict[str, Any]:
try:
# Use a safe math evaluation library in production
result = eval(args["expression"], {"__builtins__": {}})
precision = args.get("precision", 2)
formatted = round(result, precision)
return {
"content": [{
"type": "text",
"text": f"{args['expression']} = {formatted}"
}]
}
except Exception as e:
return {
"content": [{
"type": "text",
"text": f"Error: Invalid expression - {str(e)}"
}]
}
@tool(
"compound_interest",
"Calculate compound interest for an investment",
{"principal": float, "rate": float, "time": float, "n": int}
)
async def compound_interest(args: dict[str, Any]) -> dict[str, Any]:
principal = args["principal"]
rate = args["rate"]
time = args["time"]
n = args.get("n", 12)
amount = principal * (1 + rate / n) ** (n * time)
interest = amount - principal
return {
"content": [{
"type": "text",
"text": f"""Investment Analysis:
Principal: ${principal:.2f}
Rate: {rate * 100:.2f}%
Time: {time} years
Compounding: {n} times per year
Final Amount: ${amount:.2f}
Interest Earned: ${interest:.2f}
Return: {(interest / principal) * 100:.2f}%"""
}]
}
calculator_server = create_sdk_mcp_server(
name="calculator",
version="1.0.0",
tools=[calculate, compound_interest] # Pass decorated functions
)
```
## Related Documentation
* [TypeScript SDK Reference](/en/api/agent-sdk/typescript)
* [Python SDK Reference](/en/api/agent-sdk/python)
* [MCP Documentation](https://modelcontextprotocol.io)
* [SDK Overview](/en/api/agent-sdk/overview)

View File

@@ -0,0 +1,374 @@
# Hook Patterns and Configuration
This guide covers hook patterns for intercepting and modifying Claude Agent SDK behavior.
## Overview
Hooks allow you to intercept SDK events and modify behavior at key points in execution. Common uses:
- Control tool execution (approve/deny/modify)
- Add context to prompts
- Review tool outputs
- Stop execution on errors
- Log activity
> **⚠️ IMPORTANT:** The Python SDK does **NOT** support `SessionStart`, `SessionEnd`, or `Notification` hooks due to setup limitations. Only the 6 hook types listed below are supported. Attempting to use unsupported hooks will result in them never firing.
## Hook Types
| Hook | When It Fires | Common Uses |
|------|---------------|-------------|
| `PreToolUse` | Before tool execution | Approve/deny/modify tool calls |
| `PostToolUse` | After tool execution | Review output, add context, stop on errors |
| `UserPromptSubmit` | Before processing user prompt | Add context, modify prompt |
| `Stop` | When execution stops | Cleanup, final logging |
| `SubagentStop` | When a subagent stops | Capture subagent results, cleanup |
| `PreCompact` | Before message compaction | Review/modify messages before compaction |
## Hook Configuration
Hooks are configured via the `hooks` parameter in `ClaudeAgentOptions`:
```python
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[check_bash_command]),
],
"PostToolUse": [
HookMatcher(matcher="Bash", hooks=[review_output]),
],
}
)
```
## Hook Function Signature
All hooks have the same signature:
```python
from claude_agent_sdk.types import HookInput, HookContext, HookJSONOutput
async def hook_function(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""
Args:
input_data: Hook-specific data (tool_name, tool_input, etc.)
tool_use_id: Unique ID for this tool use (if applicable)
context: Additional context about the execution
Returns:
HookJSONOutput: Dict with hook-specific fields
"""
return {} # Empty dict = no action
```
## HookJSONOutput Fields
| Field | Type | Use Case | Description |
|-------|------|----------|-------------|
| `continue_` | `bool` | Stop execution | Set to `False` to halt execution |
| `stopReason` | `str` | Stop execution | Explanation for why execution stopped |
| `reason` | `str` | Logging/debugging | Explanation of hook decision |
| `systemMessage` | `str` | User feedback | Message shown to user |
| `hookSpecificOutput` | `dict` | Hook-specific data | Additional hook-specific fields |
### PreToolUse Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow" | "deny", # Control tool execution
"permissionDecisionReason": "Explanation for decision",
"modifiedInput": {...} # Optional: Modified tool input
}
}
```
### PostToolUse Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Extra context based on tool output"
}
}
```
### UserPromptSubmit Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"updatedPrompt": "Modified prompt text"
}
}
```
### Stop Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "Stop"
}
}
```
### SubagentStop Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "SubagentStop"
}
}
```
### PreCompact Hook-Specific Fields
```python
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "Context to preserve during compaction"
}
}
```
## Common Patterns
### 1. Block Dangerous Commands (PreToolUse)
```python
async def check_bash_command(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Prevent dangerous bash commands."""
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
dangerous_patterns = ["rm -rf", "sudo", "chmod 777", "dd if="]
for pattern in dangerous_patterns:
if pattern in command:
return {
"reason": f"Blocked dangerous command pattern: {pattern}",
"systemMessage": f"🚫 Blocked: {pattern}",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Command contains: {pattern}"
}
}
return {} # Allow by default
```
### 2. Review Tool Output (PostToolUse)
```python
async def review_tool_output(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Add context based on tool output."""
tool_response = input_data.get("tool_response", "")
if "error" in str(tool_response).lower():
return {
"systemMessage": "⚠️ Command produced an error",
"reason": "Tool execution failed",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Consider checking command syntax or permissions."
}
}
return {}
```
### 4. Stop on Critical Errors (PostToolUse)
```python
async def stop_on_critical_error(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Halt execution on critical errors."""
tool_response = input_data.get("tool_response", "")
if "critical" in str(tool_response).lower():
return {
"continue_": False,
"stopReason": "Critical error detected - halting for safety",
"systemMessage": "🛑 Execution stopped: critical error"
}
return {"continue_": True}
```
### 5. Redirect File Writes (PreToolUse)
```python
async def safe_file_writes(
input_data: HookInput,
tool_use_id: str | None,
context: HookContext
) -> HookJSONOutput:
"""Redirect writes to safe directory."""
if input_data["tool_name"] not in ["Write", "Edit"]:
return {}
file_path = input_data["tool_input"].get("file_path", "")
# Block writes to system directories
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
return {
"reason": f"Blocked write to system directory: {file_path}",
"systemMessage": "🚫 Cannot write to system directories",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "System directory protection"
}
}
# Redirect to safe directory
if not file_path.startswith("./safe/"):
safe_path = f"./safe/{file_path.split('/')[-1]}"
modified_input = input_data["tool_input"].copy()
modified_input["file_path"] = safe_path
return {
"reason": f"Redirected write from {file_path} to {safe_path}",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Redirected to safe directory",
"modifiedInput": modified_input
}
}
return {}
```
## Hook Matcher Patterns
`HookMatcher` determines when hooks fire:
```python
# Match specific tool
HookMatcher(matcher="Bash", hooks=[check_bash])
# Match all tools
HookMatcher(matcher=None, hooks=[log_all_tools])
# Multiple hooks for same matcher
HookMatcher(
matcher="Write",
hooks=[check_permissions, log_write, backup_file]
)
```
## Complete Example
```python
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, HookMatcher
async def block_dangerous_bash(input_data, tool_use_id, context):
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
if "rm -rf" in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Dangerous rm -rf detected"
}
}
return {}
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Read", "Write"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[block_dangerous_bash])
]
}
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Run a safe bash command")
async for message in client.receive_response():
# Process messages
pass
```
## Best Practices
1. **Return early** - Return empty dict `{}` when hook doesn't apply
2. **Be specific** - Clear `reason` and `systemMessage` fields help debugging
3. **Use matchers** - Filter hooks to relevant tools with `matcher` parameter
4. **Chain hooks** - Multiple hooks can process same event
5. **Handle errors** - Hooks should be defensive and handle missing data
6. **Log decisions** - Use `reason` field to explain hook decisions
## Anti-Patterns
**Blocking without explanation**
```python
return {
"hookSpecificOutput": {
"permissionDecision": "deny"
# Missing permissionDecisionReason
}
}
```
**Clear explanations**
```python
return {
"reason": "Command contains dangerous pattern: rm -rf",
"systemMessage": "🚫 Blocked dangerous command",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Safety: rm -rf detected"
}
}
```
**Ignoring tool_name in PreToolUse**
```python
# This will fire for ALL tools
async def hook(input_data, tool_use_id, context):
command = input_data["tool_input"]["command"] # Crashes on non-Bash tools
```
**Filter by tool_name**
```python
async def hook(input_data, tool_use_id, context):
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
```

View File

@@ -0,0 +1,247 @@
# Session Management
> Understanding how the Claude Agent SDK handles sessions and session resumption
## Session Management
The Claude Agent SDK provides session management capabilities for handling conversation state and resumption. Sessions allow you to continue conversations across multiple interactions while maintaining full context.
## How Sessions Work
When you start a new query, the SDK automatically creates a session and returns a session ID in the initial system message. You can capture this ID to resume the session later.
### Getting the Session ID
options: { options: {
model: "claude-sonnet-4-5"
}
})
for await (const message of response) {
// The first message is a system init message with the session ID
```python
if (message.type === 'system' && message.subtype === 'init') { if (message.type === 'system' && message.subtype === 'init') {
sessionId = message.session_id
console.log(`Session started with ID: ${sessionId}`)
// You can save this ID for later resumption
}
```
// Process other messages... // Process other messages...
console.log(message)
}
// Later, you can use the saved sessionId to resume
if (sessionId) {
const resumedResponse = query({
```python
prompt: "Continue where we left off",
options: {
resume: sessionId
}
})
}
```
```python Python theme={null}
from claude_agent_sdk import query, ClaudeAgentOptions
session_id = None
async for message in query(
prompt="Help me build a web application",
```
```python
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
model="claude-sonnet-4-5"
)
):
## The first message is a system init message with the session ID
```
```python
if hasattr(message, 'subtype') and message.subtype == 'init': if hasattr(message, 'subtype') and message.subtype == 'init':
session_id = message.data.get('session_id')
print(f"Session started with ID: {session_id}")
# You can save this ID for later resumption
```
## Process other messages... # Process other messages
print(message)
## Later, you can use the saved session_id to resume
if session_id:
async for message in query(
```python
prompt="Continue where we left off", prompt="Continue where we left off",
options=ClaudeAgentOptions(
resume=session_id
)
):
print(message)
```
## Resuming Sessions
The SDK supports resuming sessions from previous conversation states, enabling continuous development workflows. Use the `resume` option with a session ID to continue a previous conversation.
options: { options: {
resume: "session-xyz", // Session ID from previous conversation
model: "claude-sonnet-4-5",
allowedTools: ["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
}
})
// The conversation continues with full context from the previous session
for await (const message of response) {
console.log(message)
}
```
```python Python theme={null}
from claude_agent_sdk import query, ClaudeAgentOptions
## Resume a previous session using its ID
async for message in query(
prompt="Continue implementing the authentication system from where we left off",
```
```python
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
resume="session-xyz", # Session ID from previous conversation
model="claude-sonnet-4-5",
allowed_tools=["Read", "Edit", "Write", "Glob", "Grep", "Bash"]
)
):
print(message)
## The conversation continues with full context from the previous session
```
The SDK automatically handles loading the conversation history and context when you resume a session, allowing Claude to continue exactly where it left off.
## Forking Sessions
When resuming a session, you can choose to either continue the original session or fork it into a new branch. By default, resuming continues the original session. Use the `forkSession` option (TypeScript) or `fork_session` option (Python) to create a new session ID that starts from the resumed state.
### When to Fork a Session
Forking is useful when you want to:
* Explore different approaches from the same starting point
* Create multiple conversation branches without modifying the original
* Test changes without affecting the original session history
* Maintain separate conversation paths for different experiments
### Forking vs Continuing
| Behavior | `forkSession: false` (default) | `forkSession: true` |
| -------------------- | ------------------------------ | ------------------------------------ |
| **Session ID** | Same as original | New session ID generated |
| **History** | Appends to original session | Creates new branch from resume point |
| **Original Session** | Modified | Preserved unchanged |
| **Use Case** | Continue linear conversation | Branch to explore alternatives |
### Example: Forking a Session
options: { model: "claude-sonnet-4-5" } options: { model: "claude-sonnet-4-5" }
})
for await (const message of response) {
if (message.type === 'system' && message.subtype === 'init') {
```python
sessionId = message.session_id sessionId = message.session_id
console.log(`Original session: ${sessionId}`)
}
// Fork the session to try a different approach
const forkedResponse = query({
prompt: "Now let's redesign this as a GraphQL API instead",
```python
options: { options: {
resume: sessionId,
forkSession: true, // Creates a new session ID
model: "claude-sonnet-4-5"
}
})
for await (const message of forkedResponse) {
if (message.type === 'system' && message.subtype === 'init') {
```python
console.log(`Forked session: ${message.session_id}`) console.log(`Forked session: ${message.session_id}`)
// This will be a different session ID
}
// The original session remains unchanged and can still be resumed
const originalContinued = query({
prompt: "Add authentication to the REST API",
```python
options: { options: {
resume: sessionId,
forkSession: false, // Continue original session (default)
model: "claude-sonnet-4-5"
}
})
```
```python Python theme={null}
from claude_agent_sdk import query, ClaudeAgentOptions
## First, capture the session ID
session_id = None
async for message in query(
prompt="Help me design a REST API",
```python
options=ClaudeAgentOptions(model="claude-sonnet-4-5") options=ClaudeAgentOptions(model="claude-sonnet-4-5")
):
if hasattr(message, 'subtype') and message.subtype == 'init':
```python
session_id = message.data.get('session_id') session_id = message.data.get('session_id')
print(f"Original session: {session_id}")
## Fork the session to try a different approach
async for message in query(
prompt="Now let's redesign this as a GraphQL API instead",
```python
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
resume=session_id,
fork_session=True, # Creates a new session ID
model="claude-sonnet-4-5"
)
):
if hasattr(message, 'subtype') and message.subtype == 'init':
```python
forked_id = message.data.get('session_id') forked_id = message.data.get('session_id')
print(f"Forked session: {forked_id}")
## This will be a different session ID
## The original session remains unchanged and can still be resumed
async for message in query(
prompt="Add authentication to the REST API",
```python
options=ClaudeAgentOptions( options=ClaudeAgentOptions(
resume=session_id,
fork_session=False, # Continue original session (default)
model="claude-sonnet-4-5"
)
):
print(message)
```

View File

@@ -0,0 +1,286 @@
# Agent Skills in the SDK
> Extend Claude with specialized capabilities using Agent Skills in the Claude Agent SDK
## Overview
Agent Skills extend Claude with specialized capabilities that Claude autonomously invokes when relevant. Skills are packaged as `SKILL.md` files containing instructions, descriptions, and optional supporting resources.
For comprehensive information about Skills, including benefits, architecture, and authoring guidelines, see the [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview).
## How Skills Work with the SDK
When using the Claude Agent SDK, Skills are:
1. **Defined as filesystem artifacts**: Created as `SKILL.md` files in specific directories (`.claude/skills/`)
2. **Loaded from filesystem**: Skills are loaded from configured filesystem locations. You must specify `settingSources` (TypeScript) or `setting_sources` (Python) to load Skills from the filesystem
3. **Automatically discovered**: Once filesystem settings are loaded, Skill metadata is discovered at startup from user and project directories; full content loaded when triggered
4. **Model-invoked**: Claude autonomously chooses when to use them based on context
5. **Enabled via allowed\_tools**: Add `"Skill"` to your `allowed_tools` to enable Skills
Unlike subagents (which can be defined programmatically), Skills must be created as filesystem artifacts. The SDK does not provide a programmatic API for registering Skills.
**Default behavior**: By default, the SDK does not load any filesystem settings. To use Skills, you must explicitly configure `settingSources: ['user', 'project']` (TypeScript) or `setting_sources=["user", "project"]` (Python) in your options.
## Using Skills with the SDK
To use Skills with the SDK, you need to:
1. Include `"Skill"` in your `allowed_tools` configuration
2. Configure `settingSources`/`setting_sources` to load Skills from the filesystem
Once configured, Claude automatically discovers Skills from the specified directories and invokes them when relevant to the user's request.
```python Python theme={null}
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
```
```python
cwd="/path/to/project", # Project with .claude/skills/ cwd="/path/to/project", # Project with .claude/skills/
setting_sources=["user", "project"], # Load Skills from filesystem
allowed_tools=["Skill", "Read", "Write", "Bash"] # Enable Skill tool
)
```
```python
async for message in query( async for message in query(
prompt="Help me process this PDF document",
options=options
):
print(message)
asyncio.run(main())
```
options: { options: {
cwd: "/path/to/project", // Project with .claude/skills/
settingSources: ["user", "project"], // Load Skills from filesystem
allowedTools: ["Skill", "Read", "Write", "Bash"] // Enable Skill tool
}
})) {
console.log(message);
}
```text
## Skill Locations
Skills are loaded from filesystem directories based on your `settingSources`/`setting_sources` configuration:
* **Project Skills** (`.claude/skills/`): Shared with your team via git - loaded when `setting_sources` includes `"project"`
* **User Skills** (`~/.claude/skills/`): Personal Skills across all projects - loaded when `setting_sources` includes `"user"`
* **Plugin Skills**: Bundled with installed Claude Code plugins
## Creating Skills
Skills are defined as directories containing a `SKILL.md` file with YAML frontmatter and Markdown content. The `description` field determines when Claude invokes your Skill.
**Example directory structure**:
```bash theme={null}
.claude/skills/processing-pdfs/
└── SKILL.md
```
For complete guidance on creating Skills, including SKILL.md structure, multi-file Skills, and examples, see:
* [Agent Skills in Claude Code](/en/docs/claude-code/skills): Complete guide with examples
* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines and naming conventions
## Tool Restrictions
The `allowed-tools` frontmatter field in SKILL.md is only supported when using Claude Code CLI directly. **It does not apply when using Skills through the SDK**.
When using the SDK, control tool access through the main `allowedTools` option in your query configuration.
To restrict tools for Skills in SDK applications, use the `allowedTools` option:
Import statements from the first example are assumed in the following code snippets.
```python Python theme={null}
options = ClaudeAgentOptions(
setting_sources=["user", "project"], # Load Skills from filesystem
```
```python
allowed_tools=["Skill", "Read", "Grep", "Glob"] # Restricted toolset allowed_tools=["Skill", "Read", "Grep", "Glob"] # Restricted toolset
)
async for message in query(
prompt="Analyze the codebase structure",
```python
options=options options=options
):
print(message)
```
options: { options: {
settingSources: ["user", "project"], // Load Skills from filesystem
allowedTools: ["Skill", "Read", "Grep", "Glob"] // Restricted toolset
}
})) {
console.log(message);
}
```text
## Discovering Available Skills
To see which Skills are available in your SDK application, simply ask Claude:
```python Python theme={null}
options = ClaudeAgentOptions(
setting_sources=["user", "project"], # Load Skills from filesystem
```python
allowed_tools=["Skill"] allowed_tools=["Skill"]
)
async for message in query(
prompt="What Skills are available?",
```python
options=options options=options
):
print(message)
```
options: { options: {
settingSources: ["user", "project"], // Load Skills from filesystem
allowedTools: ["Skill"]
}
})) {
console.log(message);
}
```text
Claude will list the available Skills based on your current working directory and installed plugins.
## Testing Skills
Test Skills by asking questions that match their descriptions:
```python Python theme={null}
options = ClaudeAgentOptions(
cwd="/path/to/project",
```python
setting_sources=["user", "project"], # Load Skills from filesystem setting_sources=["user", "project"], # Load Skills from filesystem
allowed_tools=["Skill", "Read", "Bash"]
)
async for message in query(
prompt="Extract text from invoice.pdf",
```python
options=options options=options
):
print(message)
```
options: { options: {
cwd: "/path/to/project",
settingSources: ["user", "project"], // Load Skills from filesystem
allowedTools: ["Skill", "Read", "Bash"]
}
})) {
console.log(message);
}
```text
Claude automatically invokes the relevant Skill if the description matches your request.
## Troubleshooting
### Skills Not Found
**Check settingSources configuration**: Skills are only loaded when you explicitly configure `settingSources`/`setting_sources`. This is the most common issue:
```python Python theme={null}
## Wrong - Skills won't be loaded
options = ClaudeAgentOptions(
allowed_tools=["Skill"]
)
## Correct - Skills will be loaded
options = ClaudeAgentOptions(
setting_sources=["user", "project"], # Required to load Skills
```python
allowed_tools=["Skill"] allowed_tools=["Skill"]
)
```
allowedTools: ["Skill"] allowedTools: ["Skill"]
};
```text
For more details on `settingSources`/`setting_sources`, see the [TypeScript SDK reference](/en/api/agent-sdk/typescript#settingsource) or [Python SDK reference](/en/api/agent-sdk/python#settingsource).
**Check working directory**: The SDK loads Skills relative to the `cwd` option. Ensure it points to a directory containing `.claude/skills/`:
```python Python theme={null}
## Ensure your cwd points to the directory containing .claude/skills/
options = ClaudeAgentOptions(
cwd="/path/to/project", # Must contain .claude/skills/
```python
setting_sources=["user", "project"], # Required to load Skills setting_sources=["user", "project"], # Required to load Skills
allowed_tools=["Skill"]
)
```
settingSources: ["user", "project"], // Required to load Skills settingSources: ["user", "project"], // Required to load Skills
allowedTools: ["Skill"]
};
```text
See the "Using Skills with the SDK" section above for the complete pattern.
**Verify filesystem location**:
```bash theme={null}
## Check project Skills
ls .claude/skills/*/SKILL.md
## Check personal Skills
ls ~/.claude/skills/*/SKILL.md
```
### Skill Not Being Used
**Check the Skill tool is enabled**: Confirm `"Skill"` is in your `allowedTools`.
**Check the description**: Ensure it's specific and includes relevant keywords. See [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices#writing-effective-descriptions) for guidance on writing effective descriptions.
### Additional Troubleshooting
For general Skills troubleshooting (YAML syntax, debugging, etc.), see the [Claude Code Skills troubleshooting section](/en/docs/claude-code/skills#troubleshooting).
## Related Documentation
### Skills Guides
* [Agent Skills in Claude Code](/en/docs/claude-code/skills): Complete Skills guide with creation, examples, and troubleshooting
* [Agent Skills Overview](/en/docs/agents-and-tools/agent-skills/overview): Conceptual overview, benefits, and architecture
* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines for effective Skills
* [Agent Skills Cookbook](https://github.com/anthropics/claude-cookbooks/tree/main/skills): Example Skills and templates
### SDK Resources
* [Subagents in the SDK](/en/api/agent-sdk/subagents): Similar filesystem-based agents with programmatic options
* [Slash Commands in the SDK](/en/api/agent-sdk/slash-commands): User-invoked commands
* [SDK Overview](/en/api/agent-sdk/overview): General SDK concepts
* [TypeScript SDK Reference](/en/api/agent-sdk/typescript): Complete API documentation
* [Python SDK Reference](/en/api/agent-sdk/python): Complete API documentation

View File

@@ -0,0 +1,465 @@
# Slash Commands in the SDK
> Learn how to use slash commands to control Claude Code sessions through the SDK
Slash commands provide a way to control Claude Code sessions with special commands that start with `/`. These commands can be sent through the SDK to perform actions like clearing conversation history, compacting messages, or getting help.
## Discovering Available Slash Commands
The Claude Agent SDK provides information about available slash commands in the system initialization message. Access this information when your session starts:
options: { maxTurns: 1 } options: { maxTurns: 1 }
})) {
if (message.type === "system" && message.subtype === "init") {
```python
console.log("Available slash commands:", message.slash_commands); console.log("Available slash commands:", message.slash_commands);
// Example output: ["/compact", "/clear", "/help"]
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
async for message in query(
```
```python
prompt="Hello Claude", prompt="Hello Claude",
options={"max_turns": 1}
):
if message.type == "system" and message.subtype == "init":
print("Available slash commands:", message.slash_commands)
# Example output: ["/compact", "/clear", "/help"]
asyncio.run(main())
```
## Sending Slash Commands
Send slash commands by including them in your prompt string, just like regular text:
options: { maxTurns: 1 } options: { maxTurns: 1 }
})) {
if (message.type === "result") {
```python
console.log("Command executed:", message.result); console.log("Command executed:", message.result);
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
# Send a slash command
```
```python
async for message in query( async for message in query(
prompt="/compact",
options={"max_turns": 1}
):
if message.type == "result":
print("Command executed:", message.result)
asyncio.run(main())
```
## Common Slash Commands
### `/compact` - Compact Conversation History
The `/compact` command reduces the size of your conversation history by summarizing older messages while preserving important context:
options: { maxTurns: 1 } options: { maxTurns: 1 }
})) {
if (message.type === "system" && message.subtype === "compact_boundary") {
```python
console.log("Compaction completed"); console.log("Compaction completed");
console.log("Pre-compaction tokens:", message.compact_metadata.pre_tokens);
console.log("Trigger:", message.compact_metadata.trigger);
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
async for message in query(
```
```python
prompt="/compact", prompt="/compact",
options={"max_turns": 1}
):
if (message.type == "system" and
message.subtype == "compact_boundary"):
print("Compaction completed")
print("Pre-compaction tokens:",
message.compact_metadata.pre_tokens)
print("Trigger:", message.compact_metadata.trigger)
asyncio.run(main())
```
### `/clear` - Clear Conversation
The `/clear` command starts a fresh conversation by clearing all previous history:
options: { maxTurns: 1 } options: { maxTurns: 1 }
})) {
if (message.type === "system" && message.subtype === "init") {
```python
console.log("Conversation cleared, new session started"); console.log("Conversation cleared, new session started");
console.log("Session ID:", message.session_id);
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
# Clear conversation and start fresh
```python
async for message in query( async for message in query(
prompt="/clear",
options={"max_turns": 1}
):
if message.type == "system" and message.subtype == "init":
print("Conversation cleared, new session started")
print("Session ID:", message.session_id)
asyncio.run(main())
```
## Creating Custom Slash Commands
In addition to using built-in slash commands, you can create your own custom commands that are available through the SDK. Custom commands are defined as markdown files in specific directories, similar to how subagents are configured.
### File Locations
Custom slash commands are stored in designated directories based on their scope:
* **Project commands**: `.claude/commands/` - Available only in the current project
* **Personal commands**: `~/.claude/commands/` - Available across all your projects
### File Format
Each custom command is a markdown file where:
* The filename (without `.md` extension) becomes the command name
* The file content defines what the command does
* Optional YAML frontmatter provides configuration
#### Basic Example
Create `.claude/commands/refactor.md`:
```markdown theme={null}
Refactor the selected code to improve readability and maintainability.
Focus on clean code principles and best practices.
```
This creates the `/refactor` command that you can use through the SDK.
#### With Frontmatter
Create `.claude/commands/security-check.md`:
```markdown theme={null}
---
allowed-tools: Read, Grep, Glob
description: Run security vulnerability scan
model: claude-sonnet-4-5
---
Analyze the codebase for security vulnerabilities including:
- SQL injection risks
- XSS vulnerabilities
- Exposed credentials
- Insecure configurations
```
### Using Custom Commands in the SDK
Once defined in the filesystem, custom commands are automatically available through the SDK:
options: { maxTurns: 3 } options: { maxTurns: 3 }
})) {
if (message.type === "assistant") {
```python
console.log("Refactoring suggestions:", message.message); console.log("Refactoring suggestions:", message.message);
}
// Custom commands appear in the slash_commands list
for await (const message of query({
prompt: "Hello",
```python
options: { maxTurns: 1 } options: { maxTurns: 1 }
})) {
if (message.type === "system" && message.subtype === "init") {
```
// Will include both built-in and custom commands // Will include both built-in and custom commands
console.log("Available commands:", message.slash_commands);
// Example: ["/compact", "/clear", "/help", "/refactor", "/security-check"]
}
```text
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
# Use a custom command
```python
async for message in query( async for message in query(
prompt="/refactor src/auth/login.py",
options={"max_turns": 3}
):
if message.type == "assistant":
print("Refactoring suggestions:", message.message)
# Custom commands appear in the slash_commands list
async for message in query(
prompt="Hello",
options={"max_turns": 1}
):
if message.type == "system" and message.subtype == "init":
# Will include both built-in and custom commands
print("Available commands:", message.slash_commands)
# Example: ["/compact", "/clear", "/help", "/refactor", "/security-check"]
asyncio.run(main())
```
## Advanced Features
### Arguments and Placeholders
Custom commands support dynamic arguments using placeholders:
Create `.claude/commands/fix-issue.md`:
```markdown theme={null}
---
argument-hint: [issue-number] [priority]
description: Fix a GitHub issue
---
Fix issue #$1 with priority $2.
Check the issue description and implement the necessary changes.
```
Use in SDK:
options: { maxTurns: 5 } options: { maxTurns: 5 }
})) {
// Command will process with $1="123" and $2="high"
```python
if (message.type === "result") { if (message.type === "result") {
console.log("Issue fixed:", message.result);
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
# Pass arguments to custom command
```python
async for message in query( async for message in query(
prompt="/fix-issue 123 high",
options={"max_turns": 5}
):
# Command will process with $1="123" and $2="high"
if message.type == "result":
print("Issue fixed:", message.result)
asyncio.run(main())
```
## Bash Command Execution
Custom commands can execute bash commands and include their output:
Create `.claude/commands/git-commit.md`:
```markdown theme={null}
---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
description: Create a git commit
---
## Context
- Current status: !`git status`
- Current diff: !`git diff HEAD`
## Task
Create a git commit with appropriate message based on the changes.
```
### File References
Include file contents using the `@` prefix:
Create `.claude/commands/review-config.md`:
```markdown theme={null}
---
description: Review configuration files
---
Review the following configuration files for issues:
- Package config: @package.json
- TypeScript config: @tsconfig.json
- Environment config: @.env
Check for security issues, outdated dependencies, and misconfigurations.
```
### Organization with Namespacing
Organize commands in subdirectories for better structure:
```bash theme={null}
.claude/commands/
├── frontend/
│ ├── component.md # Creates /component (project:frontend)
│ └── style-check.md # Creates /style-check (project:frontend)
├── backend/
│ ├── api-test.md # Creates /api-test (project:backend)
│ └── db-migrate.md # Creates /db-migrate (project:backend)
└── review.md # Creates /review (project)
```
The subdirectory appears in the command description but doesn't affect the command name itself.
### Practical Examples
#### Code Review Command
Create `.claude/commands/code-review.md`:
```markdown theme={null}
---
allowed-tools: Read, Grep, Glob, Bash(git diff:*)
description: Comprehensive code review
---
## Changed Files
!`git diff --name-only HEAD~1`
## Detailed Changes
!`git diff HEAD~1`
## Review Checklist
Review the above changes for:
1. Code quality and readability
2. Security vulnerabilities
3. Performance implications
4. Test coverage
5. Documentation completeness
Provide specific, actionable feedback organized by priority.
```
#### Test Runner Command
Create `.claude/commands/test.md`:
```markdown theme={null}
---
allowed-tools: Bash, Read, Edit
argument-hint: [test-pattern]
description: Run tests with optional pattern
---
Run tests matching pattern: $ARGUMENTS
1. Detect the test framework (Jest, pytest, etc.)
2. Run tests with the provided pattern
3. If tests fail, analyze and fix them
4. Re-run to verify fixes
```
Use these commands through the SDK:
options: { maxTurns: 3 } options: { maxTurns: 3 }
})) {
// Process review feedback
}
// Run specific tests
for await (const message of query({
prompt: "/test auth",
```python
options: { maxTurns: 5 } options: { maxTurns: 5 }
})) {
// Handle test results
}
```
```python Python theme={null}
import asyncio
from claude_agent_sdk import query
async def main():
# Run code review
```python
async for message in query( async for message in query(
prompt="/code-review",
options={"max_turns": 3}
):
# Process review feedback
pass
# Run specific tests
async for message in query(
prompt="/test auth",
options={"max_turns": 5}
):
# Handle test results
pass
asyncio.run(main())
```
## See Also
- [Slash Commands](/en/docs/claude-code/slash-commands) - Complete slash command documentation
- [Subagents in the SDK](/en/api/agent-sdk/subagents) - Similar filesystem-based configuration for subagents
- [TypeScript SDK reference](/en/docs/claude-code/typescript-sdk-reference) - Complete API documentation
- [SDK overview](/en/api/agent-sdk/overview) - General SDK concepts
- [CLI reference](/en/docs/claude-code/cli-reference) - Command-line interface

View File

@@ -0,0 +1,302 @@
# Subagents in the SDK
> Working with subagents in the Claude Agent SDK
Subagents in the Claude Agent SDK are specialized AIs that are orchestrated by the main agent.
Use subagents for context management and parallelization.
This guide explains how to define and use subagents in the SDK using the `agents` parameter.
## Overview
Subagents can be defined in two ways when using the SDK:
1. **Programmatically** - Using the `agents` parameter in your `query()` options (recommended for SDK applications)
2. **Filesystem-based** - Placing markdown files with YAML frontmatter in designated directories (`.claude/agents/`)
This guide primarily focuses on the programmatic approach using the `agents` parameter, which provides a more integrated development experience for SDK applications.
## Benefits of Using Subagents
### Context Management
Subagents maintain separate context from the main agent, preventing information overload and keeping interactions focused. This isolation ensures that specialized tasks don't pollute the main conversation context with irrelevant details.
**Example**: A `research-assistant` subagent can explore dozens of files and documentation pages without cluttering the main conversation with all the intermediate search results - only returning the relevant findings.
### Parallelization
Multiple subagents can run concurrently, dramatically speeding up complex workflows.
**Example**: During a code review, you can run `style-checker`, `security-scanner`, and `test-coverage` subagents simultaneously, reducing review time from minutes to seconds.
### Specialized Instructions and Knowledge
Each subagent can have tailored system prompts with specific expertise, best practices, and constraints.
**Example**: A `database-migration` subagent can have detailed knowledge about SQL best practices, rollback strategies, and data integrity checks that would be unnecessary noise in the main agent's instructions.
### Tool Restrictions
Subagents can be limited to specific tools, reducing the risk of unintended actions.
**Example**: A `doc-reviewer` subagent might only have access to Read and Grep tools, ensuring it can analyze but never accidentally modify your documentation files.
## Creating Subagents
### Programmatic Definition (Recommended)
Define subagents directly in your code using the `agents` parameter:
```python
import anyio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
options = ClaudeAgentOptions(
agents={
"code-reviewer": AgentDefinition(
description="Expert code review specialist. Use for quality, security, and maintainability reviews.",
prompt="""You are a code review specialist with expertise in security, performance, and best practices.
When reviewing code:
- Identify security vulnerabilities
- Check for performance issues
- Verify adherence to coding standards
- Suggest specific improvements
Be thorough but concise in your feedback.""",
tools=["Read", "Grep", "Glob"],
model="sonnet"
),
"test-runner": AgentDefinition(
description="Runs and analyzes test suites. Use for test execution and coverage analysis.",
prompt="""You are a test execution specialist. Run tests and provide clear analysis of results.
Focus on:
- Running test commands
- Analyzing test output
- Identifying failing tests
- Suggesting fixes for failures""",
tools=["Bash", "Read", "Grep"]
)
}
)
async for message in query(
prompt="Review the authentication module for security issues",
options=options
):
print(message)
anyio.run(main)
```
### AgentDefinition Configuration
| Field | Type | Required | Description |
| :------------ | :------------------------------------------- | :------- | :--------------------------------------------------------------- |
| `description` | `string` | Yes | Natural language description of when to use this agent |
| `prompt` | `string` | Yes | The agent's system prompt defining its role and behavior |
| `tools` | `string[]` | No | Array of allowed tool names. If omitted, inherits all tools |
| `model` | `'sonnet' \| 'opus' \| 'haiku' \| 'inherit'` | No | Model override for this agent. Defaults to main model if omitted |
### Filesystem-Based Definition (Alternative)
You can also define subagents as markdown files in specific directories:
* **Project-level**: `.claude/agents/*.md` - Available only in the current project
* **User-level**: `~/.claude/agents/*.md` - Available across all projects
Each subagent is a markdown file with YAML frontmatter:
```markdown theme={null}
---
name: code-reviewer
description: Expert code review specialist. Use for quality, security, and maintainability reviews.
tools: Read, Grep, Glob, Bash
---
Your subagent's system prompt goes here. This defines the subagent's
role, capabilities, and approach to solving problems.
```
**Note:** Programmatically defined agents (via the `agents` parameter) take precedence over filesystem-based agents with the same name.
## How the SDK Uses Subagents
When using the Claude Agent SDK, subagents can be defined programmatically or loaded from the filesystem. Claude will:
1. **Load programmatic agents** from the `agents` parameter in your options
2. **Auto-detect filesystem agents** from `.claude/agents/` directories (if not overridden)
3. **Invoke them automatically** based on task matching and the agent's `description`
4. **Use their specialized prompts** and tool restrictions
5. **Maintain separate context** for each subagent invocation
Programmatically defined agents (via `agents` parameter) take precedence over filesystem-based agents with the same name.
## Example Subagents
For comprehensive examples of subagents including code reviewers, test runners, debuggers, and security auditors, see the [main Subagents guide](/en/docs/claude-code/sub-agents#example-subagents). The guide includes detailed configurations and best practices for creating effective subagents.
## SDK Integration Patterns
### Automatic Invocation
The SDK will automatically invoke appropriate subagents based on the task context. Ensure your agent's `description` field clearly indicates when it should be used:
```python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
options = ClaudeAgentOptions(
agents={
"performance-optimizer": AgentDefinition(
description="Use PROACTIVELY when code changes might impact performance. MUST BE USED for optimization tasks.",
prompt="You are a performance optimization specialist...",
tools=["Read", "Edit", "Bash", "Grep"],
model="sonnet"
)
}
)
async for message in query(
prompt="Optimize the database queries in the API layer",
options=options
):
# Process messages
pass
asyncio.run(main())
```
### Explicit Invocation
Users can request specific subagents in their prompts:
```python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
options = ClaudeAgentOptions(
agents={
"code-reviewer": AgentDefinition(
description="Expert code review specialist",
prompt="You are a security-focused code reviewer...",
tools=["Read", "Grep", "Glob"]
)
}
)
async for message in query(
prompt="Use the code-reviewer agent to check the authentication module",
options=options
):
# Process messages
pass
asyncio.run(main())
```
### Dynamic Agent Configuration
You can dynamically configure agents based on your application's needs:
```python
import asyncio
from typing import Literal
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
def create_security_agent(
security_level: Literal["basic", "strict"]
) -> AgentDefinition:
"""Create a security agent with configurable strictness."""
strictness = "strict" if security_level == "strict" else "balanced"
return AgentDefinition(
description="Security code reviewer",
prompt=f"You are a {strictness} security reviewer...",
tools=["Read", "Grep", "Glob"],
model="opus" if security_level == "strict" else "sonnet"
)
async def main():
options = ClaudeAgentOptions(
agents={
"security-reviewer": create_security_agent("strict")
}
)
async for message in query(
prompt="Review this PR for security issues",
options=options
):
# Process messages
pass
asyncio.run(main())
```
## Tool Restrictions
Subagents can have restricted tool access via the `tools` field:
* **Omit the field** - Agent inherits all available tools (default)
* **Specify tools** - Agent can only use listed tools
Example of a read-only analysis agent:
```python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
async def main():
options = ClaudeAgentOptions(
agents={
"code-analyzer": AgentDefinition(
description="Static code analysis and architecture review",
prompt="""You are a code architecture analyst. Analyze code structure,
identify patterns, and suggest improvements without making changes.""",
tools=["Read", "Grep", "Glob"] # No write or execute permissions
)
}
)
async for message in query(
prompt="Analyze the architecture of this codebase",
options=options
):
# Process messages
pass
asyncio.run(main())
```
### Common Tool Combinations
**Read-only agents** (analysis, review):
```python
tools=["Read", "Grep", "Glob"]
```
**Test execution agents**:
```python
tools=["Bash", "Read", "Grep"]
```
**Code modification agents**:
```python
tools=["Read", "Edit", "Write", "Grep", "Glob"]
```
## Related Documentation
* [Main Subagents Guide](/en/docs/claude-code/sub-agents) - Comprehensive subagent documentation
* [SDK Overview](/en/api/agent-sdk/overview) - Overview of Claude Agent SDK
* [Settings](/en/docs/claude-code/settings) - Configuration file reference
* [Slash Commands](/en/docs/claude-code/slash-commands) - Custom command creation

View File

@@ -0,0 +1,258 @@
# System Prompt Configuration Patterns
This guide covers different ways to configure system prompts in the Claude Agent SDK.
## Overview
System prompts define Claude's role, behavior, and capabilities. The SDK supports multiple configuration patterns.
## Configuration Types
### 1. No System Prompt (Vanilla Claude)
Claude operates without additional instructions:
```python
from claude_agent_sdk import query
async for message in query(prompt="What is 2 + 2?"):
# Process messages
pass
```
**Use when:** You want vanilla Claude behavior without specialized instructions.
### 2. String System Prompt
Custom instructions as a simple string:
```python
from claude_agent_sdk import ClaudeAgentOptions, query
options = ClaudeAgentOptions(
system_prompt="You are a helpful Python expert. Explain concepts simply."
)
async for message in query(prompt="Explain async/await", options=options):
pass
```
**Use when:** You need custom behavior for a specific task or domain.
### 3. Preset System Prompt
Use the official Claude Code preset:
```python
options = ClaudeAgentOptions(
system_prompt={"type": "preset", "preset": "claude_code"}
)
```
The `"claude_code"` preset includes:
- Tool usage patterns (Bash, Read, Write, Edit, etc.)
- Best practices for code modification
- Knowledge of the Task tool for delegating to subagents
- Git commit and PR workflows
- Security guidelines
**Use when:** Building SDK applications that orchestrate subagents or use file tools.
### 4. Preset with Append
Extend the Claude Code preset with additional instructions:
```python
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Always explain your reasoning before implementing changes."
}
)
```
**Use when:** You need Claude Code behavior plus domain-specific instructions.
## Common Patterns
### Orchestrator Agent
Main agent that delegates to subagents:
```python
options = ClaudeAgentOptions(
system_prompt="claude_code", # Knows how to use Task tool
allowed_tools=["Bash", "Task", "Read", "Write"],
agents={
"subagent-1": AgentDefinition(...),
"subagent-2": AgentDefinition(...)
}
)
```
**Critical:** Orchestrators must use `system_prompt="claude_code"` to understand how to delegate to subagents.
### Domain Expert
Specialized behavior for specific tasks:
```python
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": """You are a security auditor for Python applications.
Focus on:
- SQL injection vulnerabilities
- Command injection risks
- Authentication/authorization flaws
- Secrets management issues
Provide specific, actionable security recommendations."""
},
allowed_tools=["Read", "Grep", "Glob"]
)
```
### Constrained Agent
Agent with specific behavioral constraints:
```python
options = ClaudeAgentOptions(
system_prompt="""You are a read-only code analyzer.
IMPORTANT:
- Never modify files
- Never execute code
- Only analyze and report findings
Provide detailed analysis with file/line references.""",
allowed_tools=["Read", "Grep", "Glob"]
)
```
## Shorthand vs Dict Format
The SDK accepts both shorthand and dictionary formats for the `claude_code` preset:
```python
# Dict format (official examples prefer this)
system_prompt={"type": "preset", "preset": "claude_code"}
# Shorthand format (equivalent, but less explicit)
system_prompt="claude_code"
# With append (dict only)
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": "Additional instructions here"
}
```
**Note:** The shorthand `system_prompt="claude_code"` is a convenience that's equivalent to the full dict format. Both are valid and produce identical behavior. Official examples prefer the dict format for explicitness, but shorthand works fine for simple cases.
## Best Practices
1. **Use preset for orchestrators** - Orchestrators need `system_prompt="claude_code"` for Task tool knowledge
2. **Use append for specialization** - Extend preset with domain-specific instructions
3. **Match tools to prompt** - System prompt should align with allowed_tools
4. **Be specific** - Clear, specific instructions produce better results
5. **Test variations** - Experiment with different prompts for your use case
## Examples
### File Processing Agent
```python
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": """Process CSV files with these requirements:
- Validate data types
- Handle missing values
- Generate summary statistics
- Create visualizations using matplotlib"""
},
allowed_tools=["Read", "Write", "Bash"]
)
```
### Documentation Generator
```python
options = ClaudeAgentOptions(
system_prompt="""You are a technical documentation specialist.
Generate clear, comprehensive documentation with:
- Overview and purpose
- API reference with types
- Usage examples
- Common pitfalls and troubleshooting
Use Google-style Python docstrings.""",
allowed_tools=["Read", "Write", "Edit", "Grep"]
)
```
### Test Writer
```python
options = ClaudeAgentOptions(
system_prompt={
"type": "preset",
"preset": "claude_code",
"append": """Write pytest-based tests following these patterns:
- Use fixtures for setup/teardown
- Parametrize tests when appropriate
- Include edge cases and error conditions
- Aim for >90% code coverage"""
},
allowed_tools=["Read", "Write", "Bash"]
)
```
## Anti-Patterns
**Orchestrator without claude_code preset**
```python
# Orchestrator won't know how to use Task tool
options = ClaudeAgentOptions(
agents={...}
# Missing system_prompt="claude_code"
)
```
**Proper orchestrator configuration**
```python
options = ClaudeAgentOptions(
system_prompt="claude_code",
agents={...}
)
```
**Conflicting instructions**
```python
# Tells agent to modify files but only allows read tools
options = ClaudeAgentOptions(
system_prompt="Fix any bugs you find",
allowed_tools=["Read", "Grep"] # Can't actually fix anything
)
```
**Aligned tools and instructions**
```python
options = ClaudeAgentOptions(
system_prompt="Analyze code for bugs and report findings",
allowed_tools=["Read", "Grep", "Glob"]
)
```

View File

@@ -0,0 +1,460 @@
# Tool Permission Callbacks
This guide covers tool permission callbacks for fine-grained control over tool usage.
## Overview
Tool permission callbacks allow you to:
- Approve or deny tool usage
- Modify tool inputs before execution
- Implement complex permission logic
- Log tool usage
## Choosing Your Permission Strategy
The SDK provides two ways to control tool permissions: **permission modes** (simple) and **permission callbacks** (advanced).
### Quick Decision Guide
**Use `permission_mode` when:**
- You have simple, consistent permission policies
- You want to auto-approve/deny all file edits
- You don't need conditional logic
- You want minimal code
**Use `can_use_tool` callback when:**
- You need conditional approval logic
- You want to modify tool inputs before execution
- You need to block specific commands or patterns
- You want to log tool usage
- You need fine-grained control per tool
### Permission Mode Options
```python
from claude_agent_sdk import ClaudeAgentOptions
# Option 1: Auto-accept file edits (for automated workflows)
options = ClaudeAgentOptions(
permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit", "Bash"]
)
# Option 2: Require approval for edits (read-only automation)
options = ClaudeAgentOptions(
permission_mode="rejectEdits",
allowed_tools=["Read", "Grep", "Glob"]
)
# Option 3: Plan mode (no execution, just planning)
options = ClaudeAgentOptions(
permission_mode="plan",
allowed_tools=["Read", "Write", "Bash"]
)
# Option 4: Bypass all permissions (use with extreme caution)
options = ClaudeAgentOptions(
permission_mode="bypassPermissions",
allowed_tools=["Read", "Write", "Bash"]
)
```
### When to Use Each Mode
| Mode | Behavior | Best For |
|------|----------|----------|
| `"acceptEdits"` | Auto-approves file edits (Write, Edit, etc.) | CI/CD pipelines, automated refactoring, code generation |
| `"rejectEdits"` | Auto-rejects file edits, allows reads | Analysis tasks, read-only auditing, code review |
| `"plan"` | No execution, planning only | Previewing changes, cost estimation, planning phase |
| `"bypassPermissions"` | Bypasses all permission checks | Testing, trusted environments only (⚠️ dangerous) |
| `"default"` | Uses `can_use_tool` callback if provided | Custom permission logic (see below) |
### Combining Mode + Callback
You can use both together - the mode provides baseline behavior and the callback adds custom logic:
```python
async def custom_permissions(tool_name, input_data, context):
"""Custom logic on top of permission mode."""
# Block dangerous bash commands even if acceptEdits is set
if tool_name == "Bash":
command = input_data.get("command", "")
if "rm -rf /" in command:
return PermissionResultDeny(message="Dangerous command blocked")
return PermissionResultAllow()
options = ClaudeAgentOptions(
permission_mode="acceptEdits", # Auto-approve file edits
can_use_tool=custom_permissions, # But add custom bash validation
allowed_tools=["Read", "Write", "Edit", "Bash"]
)
```
### Simple Use Cases: Just Use permission_mode
```python
# Example 1: Automated code generation (accept all edits)
options = ClaudeAgentOptions(
permission_mode="acceptEdits",
allowed_tools=["Read", "Write", "Edit"]
)
# Example 2: Code analysis (read-only)
options = ClaudeAgentOptions(
permission_mode="rejectEdits",
allowed_tools=["Read", "Grep", "Glob"]
)
# Example 3: Planning phase (no execution)
options = ClaudeAgentOptions(
permission_mode="plan",
allowed_tools=["Read", "Write", "Bash"]
)
```
## Permission Callbacks (Advanced)
For complex permission logic, use the `can_use_tool` callback.
## Callback Signature
```python
from claude_agent_sdk import (
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext
)
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""
Args:
tool_name: Name of the tool being used
input_data: Tool input parameters
context: Additional context (suggestions, etc.)
Returns:
PermissionResultAllow or PermissionResultDeny
"""
pass
```
## Permission Results
### Allow
```python
# Simple allow
return PermissionResultAllow()
# Allow with modified input
return PermissionResultAllow(
updated_input={"file_path": "/safe/output.txt"}
)
```
### Deny
```python
return PermissionResultDeny(
message="Cannot write to system directories"
)
```
## Common Patterns
### 1. Allow Read-Only Tools
```python
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Auto-allow read-only operations."""
# Always allow read operations
if tool_name in ["Read", "Glob", "Grep"]:
return PermissionResultAllow()
# Deny or ask for other tools
return PermissionResultDeny(
message=f"Tool {tool_name} requires approval"
)
```
### 2. Block Dangerous Commands
```python
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Block dangerous bash commands."""
if tool_name == "Bash":
command = input_data.get("command", "")
dangerous = ["rm -rf", "sudo", "chmod 777", "dd if=", "mkfs"]
for pattern in dangerous:
if pattern in command:
return PermissionResultDeny(
message=f"Dangerous command pattern: {pattern}"
)
return PermissionResultAllow()
```
### 3. Redirect File Writes
```python
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Redirect writes to safe directory."""
if tool_name in ["Write", "Edit", "MultiEdit"]:
file_path = input_data.get("file_path", "")
# Block system directories
if file_path.startswith("/etc/") or file_path.startswith("/usr/"):
return PermissionResultDeny(
message=f"Cannot write to system directory: {file_path}"
)
# Redirect to safe directory
if not file_path.startswith("./safe/"):
safe_path = f"./safe/{file_path.split('/')[-1]}"
modified_input = input_data.copy()
modified_input["file_path"] = safe_path
return PermissionResultAllow(
updated_input=modified_input
)
return PermissionResultAllow()
```
### 4. Log Tool Usage
```python
tool_usage_log = []
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Log all tool usage."""
# Log the request
tool_usage_log.append({
"tool": tool_name,
"input": input_data,
"suggestions": context.suggestions
})
print(f"Tool: {tool_name}")
print(f"Input: {input_data}")
return PermissionResultAllow()
```
### 5. Interactive Approval
```python
async def permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Ask user for permission on unknown tools."""
# Auto-allow safe tools
if tool_name in ["Read", "Grep", "Glob"]:
return PermissionResultAllow()
# Auto-deny dangerous tools
if tool_name == "Bash":
command = input_data.get("command", "")
if "rm -rf" in command:
return PermissionResultDeny(message="Dangerous command")
# Ask user for other tools
print(f"\nTool: {tool_name}")
print(f"Input: {input_data}")
user_input = input("Allow? (y/N): ").strip().lower()
if user_input in ("y", "yes"):
return PermissionResultAllow()
else:
return PermissionResultDeny(message="User denied permission")
```
## Configuration
Set the callback in `ClaudeAgentOptions`:
```python
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
options = ClaudeAgentOptions(
can_use_tool=permission_callback,
permission_mode="default", # Ensure callbacks are invoked
cwd="."
)
async with ClaudeSDKClient(options) as client:
await client.query("List files and create hello.py")
async for message in client.receive_response():
# Process messages
pass
```
## Permission Modes
| Mode | Behavior | Use Case |
|------|----------|----------|
| `"default"` | Invokes callback for every tool | Fine-grained control |
| `"acceptEdits"` | Auto-approves file edits | Automated workflows |
| `"rejectEdits"` | Auto-rejects file edits | Read-only mode |
## Complete Example
```python
import asyncio
from claude_agent_sdk import (
ClaudeAgentOptions,
ClaudeSDKClient,
PermissionResultAllow,
PermissionResultDeny,
ToolPermissionContext,
)
# Track usage
tool_log = []
async def safe_permission_callback(
tool_name: str,
input_data: dict,
context: ToolPermissionContext
) -> PermissionResultAllow | PermissionResultDeny:
"""Safe permission callback with logging."""
# Log usage
tool_log.append({"tool": tool_name, "input": input_data})
# Always allow read operations
if tool_name in ["Read", "Glob", "Grep"]:
print(f"✅ Auto-allow: {tool_name}")
return PermissionResultAllow()
# Check writes to system directories
if tool_name in ["Write", "Edit"]:
file_path = input_data.get("file_path", "")
if file_path.startswith("/etc/"):
print(f"❌ Blocked: write to {file_path}")
return PermissionResultDeny(
message=f"Cannot write to system directory"
)
# Check dangerous bash commands
if tool_name == "Bash":
command = input_data.get("command", "")
if "rm -rf" in command or "sudo" in command:
print(f"❌ Blocked: dangerous command")
return PermissionResultDeny(
message="Dangerous command pattern detected"
)
print(f"✅ Allowed: {tool_name}")
return PermissionResultAllow()
async def main():
options = ClaudeAgentOptions(
can_use_tool=safe_permission_callback,
permission_mode="default",
allowed_tools=["Read", "Write", "Bash"]
)
async with ClaudeSDKClient(options) as client:
await client.query("List files and create hello.py")
async for message in client.receive_response():
# Process messages
pass
# Print usage summary
print("\nTool Usage Summary:")
for entry in tool_log:
print(f" {entry['tool']}: {entry['input']}")
if __name__ == "__main__":
asyncio.run(main())
```
## Best Practices
1. **Return early** - Check tool_name first and return quickly for unmatched tools
2. **Be defensive** - Use `.get()` to safely access input_data fields
3. **Log decisions** - Track what was allowed/denied for debugging
4. **Clear messages** - Denial messages should explain why
5. **Test thoroughly** - Verify callback logic with different tool types
## Anti-Patterns
**Assuming input structure**
```python
# Crashes if command key doesn't exist
command = input_data["command"]
```
**Safe access**
```python
command = input_data.get("command", "")
```
**Silent denials**
```python
return PermissionResultDeny() # No message
```
**Informative denials**
```python
return PermissionResultDeny(
message="Cannot write to system directories for safety"
)
```
**Checking all tools for Bash-specific logic**
```python
# This crashes on non-Bash tools
async def callback(tool_name, input_data, context):
command = input_data["command"] # Only Bash has "command"
```
**Filter by tool_name first**
```python
async def callback(tool_name, input_data, context):
if tool_name != "Bash":
return PermissionResultAllow()
command = input_data.get("command", "")
# Now safe to check command
```