Loading...
Loading...
Build AI agents and automate Claude Code programmatically using the Claude Agent SDK and headless CLI mode. Use this skill when you need to build an agent, create a Claude agent, make a bot, work with the agent SDK, run Claude in headless mode, write programmatic agent code, automate with Claude, create an MCP server builder, or query Claude programmatically. Covers the Python SDK, the claude -p headless interface, custom tool creation with SDK MCP servers, hooks for deterministic control, session management, and CLI flag reference. Authentication uses existing ~/.claude/ config — no API keys required.
npx skill4agent add mathews-tom/praxis-skills agent-builderPreferred: Claude Code CLI headless mode (claude -p)
Preferred: Claude Agent SDK (wraps Claude Code CLI)
Avoid: Raw Anthropic API (anthropic.Anthropic())
Avoid: Direct API calls with API keys~/.claude/anthropic.Anthropic()anthropic.messages.create()ANTHROPIC_API_KEYclaude -pclaude_agent_sdkclaude -p~/.claude/# Avoid — this is the raw Anthropic API, not Claude Code
from anthropic import Anthropic
client = Anthropic(api_key="sk-...") # Requires separate API key
response = client.messages.create( # Bypasses agent loop
model="claude-sonnet-4-5",
messages=[{"role": "user", "content": "Hello"}]
)# Preferred — Agent SDK wraps Claude Code CLI
from claude_agent_sdk import query, ClaudeAgentOptions
import anyio
async def main():
options = ClaudeAgentOptions(
allowed_tools=["Read", "Write", "Bash"], # Full tool access
permission_mode="acceptEdits"
)
# Uses ~/.claude/ config — no API key needed
async for message in query(prompt="Analyze this codebase", options=options):
print(message)
anyio.run(main)# Claude Code headless mode
claude -p "Analyze this codebase" \
--allowedTools "Read,Write,Bash" \
--output-format json~/.claude/# Install Claude Code CLI first (if not already installed)
curl -fsSL https://claude.ai/install.sh | bash
# Install the Agent SDK (wraps Claude Code CLI)
uv pip install claude-agent-sdk # Python 3.10+~/.claude/import anyio
from claude_agent_sdk import query
async def main():
# Uses Claude Code CLI — inherits auth from ~/.claude/
async for message in query(prompt="What is 2 + 2?"):
print(message)
anyio.run(main)from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
options = ClaudeAgentOptions(
system_prompt="You are a helpful assistant",
max_turns=10,
allowed_tools=["Read", "Write", "Bash"],
permission_mode='acceptEdits',
cwd="/path/to/project"
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Analyze this codebase")
async for msg in client.receive_response():
if msg.type == "assistant":
for block in msg.content:
if block.type == "text":
print(block.text)from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("greet", "Greet a user by name", {"name": str})
async def greet_user(args):
return {
"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]
}
@tool("fetch_data", "Fetch data from API", {"endpoint": str})
async def fetch_data(args):
# Implementation
return {"content": [{"type": "text", "text": f"Data from {args['endpoint']}"}]}
server = create_sdk_mcp_server(
name="custom-tools",
version="1.0.0",
tools=[greet_user, fetch_data]
)
options = ClaudeAgentOptions(
mcp_servers={"tools": server},
allowed_tools=["mcp__tools__greet", "mcp__tools__fetch_data"]
)from claude_agent_sdk import HookMatcher
async def validate_bash_command(input_data, tool_use_id, context):
if input_data["tool_name"] != "Bash":
return {}
command = input_data["tool_input"].get("command", "")
forbidden = ["rm -rf", "dd if=", "mkfs"]
for pattern in forbidden:
if pattern in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": f"Forbidden: {pattern}",
}
}
return {}
options = ClaudeAgentOptions(
allowed_tools=["Bash"],
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[validate_bash_command]),
],
}
)from claude_agent_sdk import (
CLINotFoundError,
CLIConnectionError,
ProcessError,
CLIJSONDecodeError,
)
try:
async for message in query(prompt="Hello"):
pass
except CLINotFoundError:
print("Install Claude Code: curl -fsSL https://claude.ai/install.sh | bash")
except ProcessError as e:
print(f"Process failed: {e.exit_code}")
except CLIJSONDecodeError as e:
print(f"JSON parse error: {e}")# Simple query
claude -p "What does the auth module do?"
# Structured JSON output
claude -p "Summarize this project" --output-format json
# Extract specific data with jq
claude -p "Summarize this project" --output-format json | jq -r '.result'claude -p "Extract function names from auth.py" \
--output-format json \
--json-schema '{
"type": "object",
"properties": {
"functions": {"type": "array", "items": {"type": "string"}}
},
"required": ["functions"]
}' | jq '.structured_output'# Stream with full events
claude -p "Explain recursion" \
--output-format stream-json \
--verbose \
--include-partial-messages
# Extract only text deltas with jq
claude -p "Write a poem" \
--output-format stream-json \
--verbose \
--include-partial-messages | \
jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'# Allow specific tools without prompting
claude -p "Run tests and fix failures" \
--allowedTools "Bash,Read,Edit"
# Git operations with prefix matching
claude -p "Review staged changes and commit" \
--allowedTools "Bash(git diff *)" "Bash(git log *)" "Bash(git status *)" "Bash(git commit *)"# First request
claude -p "Review this codebase for performance issues"
# Continue most recent
claude -p "Focus on database queries" --continue
# Resume specific session
session_id=$(claude -p "Start review" --output-format json | jq -r '.session_id')
claude -p "Continue review" --resume "$session_id"# Append to default prompt (recommended)
claude -p "Review this PR" \
--append-system-prompt "You are a security engineer. Focus on vulnerabilities."
# Replace entire prompt (full control)
claude -p "Analyze code" \
--system-prompt "You are a Python expert who only writes type-annotated code"
# Load from file
claude -p "Review code" \
--append-system-prompt-file ./prompts/style-rules.txt# Interactive REPL
claude
claude "explain this project"
# Headless/print mode
claude -p "query"
cat file | claude -p "analyze"
# Continue conversations
claude -c # most recent
claude -c -p "follow-up" # continue via SDK
claude -r "session-id" # resume specific
# Update
claude update
# MCP configuration
claude mcp| Flag | Purpose | Example |
|---|---|---|
| Auto-approve tools | |
| Block tools | |
| Restrict available tools | |
| Set model | |
| Limit turns | |
| Cost limit | |
| Output format | |
| Structured output | |
| Start in mode | |
| Additional dirs | |
| Custom subagents | |
| Browser automation | |
| Full logging | |
claude --agents '{
"code-reviewer": {
"description": "Expert code reviewer",
"prompt": "Focus on code quality, security, best practices",
"tools": ["Read", "Grep", "Glob"],
"model": "sonnet"
},
"debugger": {
"description": "Debugging specialist",
"prompt": "Analyze errors, identify root causes",
"maxTurns": 10
}
}'# Tool name only
--allowedTools "Read" "Write"
# With argument pattern (exact match)
--allowedTools "Bash(git status)"
# With prefix matching (note the space before *)
--allowedTools "Bash(git diff *)" "Bash(git log *)"
# Block destructive patterns
--disallowedTools "Bash(rm -rf *)" "Bash(dd *)"from claude_agent_sdk import query, ClaudeAgentOptions
options = ClaudeAgentOptions(
allowed_tools=["Bash", "Read", "Edit"],
permission_mode="acceptEdits",
max_turns=20,
max_budget_usd=5.0
)
async for msg in query(
prompt="Run the test suite. Fix any failures. Re-run until all pass.",
options=options
):
# Process messages
pass#!/bin/bash
gh pr diff "$1" | claude -p \
--append-system-prompt "Security review: check for OWASP top 10, injection, XSS, auth issues" \
--allowedTools "Read" \
--output-format json \
--max-turns 3 | jq -r '.result'from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeSDKClient, ClaudeAgentOptions
@tool("analyze_file", "Analyze file metrics", {"path": str})
async def analyze_file(args):
# Custom analysis logic
with open(args["path"]) as f:
lines = len(f.readlines())
return {"content": [{"type": "text", "text": f"File has {lines} lines"}]}
server = create_sdk_mcp_server(name="analyzer", version="1.0.0", tools=[analyze_file])
options = ClaudeAgentOptions(
mcp_servers={"analyzer": server},
allowed_tools=["mcp__analyzer__analyze_file", "Read"],
system_prompt="You are a code quality analyzer"
)
async with ClaudeSDKClient(options=options) as client:
await client.query("Analyze all Python files in this directory")
async for msg in client.receive_response():
print(msg)from claude_agent_sdk import tool, create_sdk_mcp_server
@tool("query_db", "Execute SQL query", {"query": str})
async def query_database(args):
# Safe parameterized query execution
result = execute_query(args["query"])
return {"content": [{"type": "text", "text": str(result)}]}
@tool("get_schema", "Get database schema", {})
async def get_schema(args):
schema = fetch_schema()
return {"content": [{"type": "text", "text": schema}]}
server = create_sdk_mcp_server(
name="db-tools",
version="1.0.0",
tools=[query_database, get_schema]
)claude_agent_sdkanthropic.Anthropic()~/.claude/anyio.run()max_turnsmax_budget_usdpermission_mode='acceptEdits'--output-format jsonjq--max-turns--allowedToolssession_id--verbose--dangerously-skip-permissions--max-budget-usdclaude -p "query"ClaudeSDKClient@toolHookMatcher--output-format stream-json--json-schema--continue--resumeclaude -p--allowedToolsquery()ClaudeSDKClientWebFetchclaude -pclaude_agent_sdk~/.claude/which claudeuv pip listclaude_agent_sdkclaude -panthropic.Anthropic()ANTHROPIC_API_KEYapi_key=import anyio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
tool,
create_sdk_mcp_server,
HookMatcher,
)
# Using claude_agent_sdk (wraps Claude Code CLI)
# Custom tool
@tool("get_config", "Get app configuration", {})
async def get_config(args):
return {"content": [{"type": "text", "text": "Config: {...}"}]}
# Hook for validation
async def validate_edit(input_data, tool_use_id, context):
if input_data["tool_name"] != "Edit":
return {}
file_path = input_data["tool_input"].get("file_path", "")
if file_path.endswith(".env"):
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot edit .env files",
}
}
return {}
# Setup
server = create_sdk_mcp_server("config-tools", "1.0.0", [get_config])
options = ClaudeAgentOptions(
system_prompt="You are a configuration management assistant",
allowed_tools=["Read", "Edit", "mcp__config_tools__get_config"],
mcp_servers={"config_tools": server},
hooks={"PreToolUse": [HookMatcher(matcher="Edit", hooks=[validate_edit])]},
max_turns=10,
max_budget_usd=1.0,
cwd="/path/to/project",
)
# Run
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("Update the app configuration")
async for msg in client.receive_response():
if msg.type == "assistant":
for block in msg.content:
if hasattr(block, "text"):
print(block.text)
anyio.run(main)~/.claude/anthropic.Anthropic()--resume@tool