| name | create-claude-agent |
| description | Create production-ready Claude agent projects using the Claude Agent SDK. Use when this capability is needed. |
| metadata | {"author":"extractumio"} |
Create Claude Agent
Instructions
- Ask user about agent's purpose and required tools (Bash, Read, Write, Edit, Grep, Glob, WebFetch, Skill)
- Detect working folder (see below)
- Check for naming conflicts (existing agent with same name)
- Create project structure using templates
- Customize
task.md for agent's task
- Customize
.claude/settings.local.json permissions based on required tools
- Initialize git repository if not exists
- Create virtual environment and install dependencies:
- Run
python3 -m venv .venv
- Activate venv and run
pip install -r requirements.txt
- Output concise confirmation with next steps (no .md explanation files)
Prerequisites
Before creating an agent, verify:
- Python 3.10+ is installed
- pip is available
- Claude CLI is installed (
npm install -g @anthropic-ai/claude-code or brew install claude)
ANTHROPIC_API_KEY is set in .env file
Working Folder Detection
Before creating files, detect the project structure:
- Check if folder is empty or has existing structure (look for
src/, agents/, scripts/, etc.)
- Determine agent location based on structure:
| Condition | Agent Location | Project Root |
|---|
| Empty folder | ./src/, ./logs/, ./claude_agent.py | Current dir |
Has src/ | src/agents/{agent-name}/ | Current dir |
Has agents/ | agents/{agent-name}/ | Current dir |
Has scripts/ | scripts/{agent-name}/ | Current dir |
| Other | ./{agent-name}/ | Current dir |
- Check for naming conflicts — if agent location exists, prompt user to overwrite, rename, or abort
- Place project-level files at root:
.vscode/launch.json → project root (merge if exists)
requirements.txt → project root (merge if exists)
.gitignore → project root (merge if exists)
.env.example → project root (create if not exists)
Merging Existing Files
When project-level files already exist, merge rather than overwrite:
.vscode/launch.json: Append new configuration to configurations array (avoid duplicates by name)
requirements.txt: Append only missing packages
.gitignore: Append only missing patterns
Post-Creation Steps
After creating files:
- Initialize git (if not already a repo)
- Create virtual environment:
python3 -m venv .venv
- Activate and install dependencies:
source .venv/bin/activate
pip install -r requirements.txt
Project Structure
{project-root}/
├── .vscode/
│ └── launch.json # Debug config
├── .gitignore
├── requirements.txt
├── {agent-location}/ # Adaptive (see above)
│ ├── .claude/
│ │ └── settings.local.json
│ ├── src/
│ │ ├── __init__.py
│ │ ├── agent.py
│ │ ├── schemas.py
│ │ └── exceptions.py
│ ├── logs/ # Execution logs
│ ├── output/ # Message history (JSONL)
│ ├── task.md
│ └── claude_agent.py
File Templates
requirements.txt
claude-agent-sdk
pydantic
python-dotenv
colorlog
Note: Use claude-agent-sdk. The SDK wraps the Claude CLI.
.env.example
# Required: Your Anthropic API key
ANTHROPIC_API_KEY=your-api-key-here
# Optional: Agent configuration
# AGENT_MODEL=claude-sonnet-4-5
# AGENT_MAX_TURNS=50
# AGENT_TIMEOUT_SECONDS=1800
# AGENT_MAX_BUDGET_USD=10.0
# AGENT_PERMISSION_MODE=bypassPermissions # default: bypassPermissions (allows all tools)
.gitignore
# Agent logs and output
logs/
output/
*.jsonl
# Environment and secrets
.env
.env.local
# Python
__pycache__/
*.pyc
*.pyo
.venv/
venv/
*.egg-info/
# IDE
.idea/
*.swp
*.swo
.claude/settings.local.json
Customize permissions based on agent's required capabilities:
{
"permissions": {
"allow": [
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(git:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(cat:*)",
"Bash(python:*)",
"Bash(jq:*)",
"Bash(date:*)"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(sudo:*)",
"Bash(chmod 777:*)",
"Bash(> /dev:*)"
]
}
}
Permission Customization Guide:
| Agent Type | Additional Allow | Additional Deny |
|---|
| Web scraper | Bash(node:*) | Bash(ssh:*) |
| Data processor | Bash(jq:*), Bash(awk:*) | Bash(curl:*) |
| DevOps | Bash(docker:*), Bash(kubectl:*) | — |
| File manager | Bash(mv:*), Bash(cp:*) | Bash(curl:*) |
Security: Start with minimal permissions, add as needed. Never allow sudo or destructive operations without explicit user confirmation.
.vscode/launch.json
Use {agent-path} relative to project root. Uses cross-platform Python path.
Critical: Do NOT use envFile - the SDK uses Claude CLI authentication, not environment variables.
{
"version": "0.2.0",
"configurations": [
{
"name": "Run {agent-name}",
"type": "debugpy",
"request": "launch",
"python": "${command:python.interpreterPath}",
"program": "${workspaceFolder}/{agent-path}/claude_agent.py",
"args": [],
"console": "integratedTerminal",
"justMyCode": true,
"env": {
"PYTHONPATH": "${workspaceFolder}/{agent-path}",
"PYTHONUNBUFFERED": "1",
},
Cross-platform note: Using ${command:python.interpreterPath} ensures compatibility with Windows and Unix systems. Requires Python extension in VS Code/Cursor.
task.md
# Agent Task
## Objective
Describe what the agent should accomplish.
## Instructions
1. Step one
2. Step two
3. Step three
## Expected Output
Describe what output.json should contain.
src/init.py
"""Agent source package."""
src/exceptions.py
"""Agent exceptions."""
class AgentError(Exception):
"""Base exception."""
pass
class ConfigurationError(AgentError):
"""Configuration error."""
pass
class SessionIncompleteError(AgentError):
"""Session did not complete."""
pass
class MaxTurnsExceededError(AgentError):
"""Exceeded turn limit."""
pass
class ServerError(AgentError):
"""API error."""
pass
src/schemas.py
"""Data models."""
from enum import StrEnum
from typing import Any, Optional
from pydantic import BaseModel, Field
class TaskStatus(StrEnum):
SUCCESS = "SUCCESS"
PARTIAL = "PARTIAL"
FAILED = "FAILED"
ERROR = "ERROR"
class LLMMetrics(BaseModel):
"""Session metrics."""
model: str
duration_ms: int
num_turns: int
session_id: str
total_cost_usd: Optional[float] = None
class AgentResult(BaseModel):
"""Agent execution result."""
status: TaskStatus
summary: str
details: Optional[str] = None
data: dict[str, Any] = Field(default_factory=dict)
metrics: Optional[LLMMetrics] = None
error: Optional[str] = None
message_history: list[dict[str, Any]] = Field(default_factory=list)
src/agent.py
"""Agent execution logic."""
import asyncio
import logging
import os
import time
from typing import Any, Optional
from claude_agent_sdk import query
from claude_agent_sdk.types import (
AssistantMessage,
ClaudeAgentOptions,
ResultMessage,
SystemMessage,
UserMessage,
)
from .exceptions import (
AgentError,
MaxTurnsExceededError,
ServerError,
SessionIncompleteError,
)
from .schemas import AgentResult, LLMMetrics, TaskStatus
logger = logging.getLogger(__name__)
BLUE = "\033[94m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
CYAN = "\033[96m"
MAGENTA = "\033[95m"
RESET = "\033[0m"
BOLD = "\033[1m"
def serialize_block(block: Any) -> dict[str, Any]:
"""Convert SDK message block to serializable dict."""
block_type = getattr(block, 'type', None)
if block_type == 'tool_use':
return {
"type": "tool_use",
"name": getattr(block, 'name', ''),
"input": (block, , {}),
}
block_type == :
content = (block, , )
{
: ,
: (block, , ),
: (content)[:] content ,
}
block_type == :
{: , : (block, , )}
{: (block_type), : (block)[:]}
() -> :
(, flush=)
() -> AgentResult:
max_turns = (os.getenv(, ))
timeout_seconds = (os.getenv(, ))
max_budget = (os.getenv(, ))
model = os.getenv(, )
permission_mode = os.getenv(, )
logger.info()
options = ClaudeAgentOptions(
model=model,
cwd=working_dir,
permission_mode=permission_mode,
max_turns=max_turns,
max_budget_usd=max_budget,
add_dirs=additional_dirs [],
)
:
result_text =
session_id =
num_turns =
total_cost =
message_history: [[, ]] = []
():
result_text, session_id, num_turns, total_cost
turn_count =
message query(prompt=task, options=options):
(message, AssistantMessage):
turn_count +=
content = (message, , )
serialized_blocks: [[, ]] = []
content:
msg_content = (content, , )
msg_content:
block msg_content:
serialized_blocks.append(serialize_block(block))
verbose:
block_type = (block, , )
block_type == :
text = (block, , )
text:
preview = text[:] + ( (text) > )
print_verbose(, preview, GREEN)
block_type == :
tool_name = (block, , )
tool_input = (block, , {})
print_verbose(, , YELLOW)
tool_name == tool_input:
print_verbose(, tool_input[][:], MAGENTA)
message_history.append({
: turn_count,
: ,
: time.time(),
: serialized_blocks,
})
verbose:
print_verbose(, , CYAN)
(message, UserMessage):
content = (message, , )
content:
msg_content = (content, , )
msg_content verbose:
block msg_content:
block_type = (block, , )
block_type == :
result_content = (block, , )
(result_content, ) result_content:
preview = result_content[:] + ( (result_content) > )
print_verbose(, preview, BLUE)
(message, SystemMessage):
verbose:
print_verbose(, (message)[:], CYAN)
(message, ResultMessage):
result_text = (message, , (message))
session_id = (message, , )
num_turns = (message, , turn_count)
total_cost = (message, , )
verbose:
cost_str = total_cost
print_verbose(, , GREEN)
asyncio.wait_for(run_query(), timeout=timeout_seconds)
metrics = LLMMetrics(
model=model,
duration_ms=,
num_turns=num_turns,
session_id=session_id,
total_cost_usd=total_cost,
)
AgentResult(
status=TaskStatus.SUCCESS,
summary=,
details=result_text,
metrics=metrics,
message_history=message_history,
)
asyncio.TimeoutError:
AgentError()
Exception e:
error_msg = (e)
error_msg.lower():
MaxTurnsExceededError()
error_msg.lower() error_msg.lower():
ServerError()
error_msg.lower():
SessionIncompleteError()
AgentError()
claude_agent.py
"""
Claude Agent entry point.
Usage:
python claude_agent.py # Run with default task.md
python claude_agent.py --task-file x.md # Custom task file
python claude_agent.py --output out.json # Save output to file
python claude_agent.py -v # Verbose output
Prerequisites:
pip install -r requirements.txt
Set ANTHROPIC_API_KEY in .env file
"""
import argparse
import asyncio
import json
import logging
import sys
from datetime import datetime
from pathlib import Path
import colorlog
from dotenv import load_dotenv
from src.agent import run_agent_with_timeout
from src.schemas import AgentResult, TaskStatus
load_dotenv()
def setup_logging(logs_dir: Path) -> logging.Logger:
"""Configure logging with console and file handlers."""
logs_dir.mkdir(parents=True, exist_ok=True)
console_handler = colorlog.StreamHandler()
console_handler.setFormatter(colorlog.ColoredFormatter(
"%(log_color)s%(asctime)s [%(levelname)s]%(reset)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
log_colors={
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red,bg_white",
},
))
log_file = logs_dir /
file_handler = logging.FileHandler(log_file, encoding=)
file_handler.setFormatter(logging.Formatter(
,
datefmt=,
))
logger = colorlog.getLogger(__name__)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.setLevel(logging.INFO)
logger
() -> :
task_file.exists():
FileNotFoundError()
task_file.read_text(encoding=)
() -> :
result.message_history:
output_dir.mkdir(parents=, exist_ok=)
output_file = output_dir /
output_file.(, encoding=) f:
msg result.message_history:
f.write(json.dumps(msg) + )
logger.info()
() -> :
base_dir = Path(__file__).parent
logs_dir = base_dir /
output_dir = base_dir /
logger = setup_logging(logs_dir)
parser = argparse.ArgumentParser(description=)
parser.add_argument(
,
default=,
=,
)
parser.add_argument(
,
=Path,
default=Path(),
=,
)
parser.add_argument(
,
=,
)
parser.add_argument(
,
action=,
default=[],
=,
)
parser.add_argument(
, ,
action=,
=,
)
args = parser.parse_args()
args.verbose:
logger.setLevel(logging.DEBUG)
:
task: = load_task_file(args.task_file)
FileNotFoundError e:
logger.error((e))
logger.info()
logger.info()
:
result = asyncio.run(
run_agent_with_timeout(
task=task,
working_dir=args.,
additional_dirs=args.add_dir,
verbose=args.verbose,
)
)
session_id = (
result.metrics.session_id
result.metrics
datetime.now().strftime()
)
save_message_history(result, output_dir, session_id, logger)
output_json: = result.model_dump_json(
indent=, exclude={}
)
args.output:
Path(args.output).write_text(output_json, encoding=)
logger.info()
:
(output_json)
result.metrics:
logger.info(
result.metrics.total_cost_usd
)
result.status == TaskStatus.SUCCESS
Exception e:
logger.error()
__name__ == :
sys.exit(main())
Auxiliary script
You may want to create additional python script as part of the agentic tools so it could use them instead of direct access to API or database. If so, add corresponding instructions to the agent promt (task.md file).
Completion Output
After creating the agent, output only:
✓ Agent "{agent-name}" created at {agent-path}/
✓ Virtual environment created and dependencies installed
To run:
# Copy .env.example to .env and set your API key
cp .env.example .env
# Edit .env and add your ANTHROPIC_API_KEY
# Activate virtual environment
source .venv/bin/activate # Unix/macOS
# .\.venv\Scripts\Activate.ps1 # Windows PowerShell
# Run the agent
python {agent-path}/claude_agent.py
Or press F5 in VS Code/Cursor (select .venv interpreter first).
Available Tools
| Tool | Capability |
|---|
Bash | Execute shell commands |
Read | Read file contents |
Write | Create/overwrite files |
Edit | Modify existing files |
Grep | Search patterns in files |
Glob | Find files by pattern |
WebFetch | Fetch content from URLs |
Skill | Execute custom skills |
Configuration Options
Environment variables for customization:
| Variable | Default | Description |
|---|
AGENT_MODEL | claude-sonnet-4-5 | Model alias |
AGENT_MAX_TURNS | 50 | Maximum conversation turns |
AGENT_TIMEOUT_SECONDS | 1800 | Execution timeout |
AGENT_MAX_BUDGET_USD | 10.0 | Maximum API cost |
AGENT_PERMISSION_MODE | bypassPermissions | Permission mode |
Valid permission modes:
default - CLI prompts for dangerous tools
acceptEdits - Auto-accept file edits
plan - Planning mode, no execution
bypassPermissions - Allow all tools (use with caution)
Valid model aliases:
claude-sonnet-4-5 - Fast, capable (recommended)
claude-opus-4 - Most capable
SDK Options (ClaudeAgentOptions):
| Parameter | Type | Description |
|---|
cwd | str | Working directory for the agent |
add_dirs | list[str] | Additional directories the agent can access (read/write). Use when agent needs files outside cwd — e.g., shared configs, data directories, or external resources |
model | str | Model alias |
permission_mode | str | Permission mode (see above) |
max_turns | int | Maximum conversation turns |
max_budget_usd | float | Maximum API cost |
Troubleshooting
Common Issues
| Problem | Cause | Solution |
|---|
ModuleNotFoundError: claude_agent_sdk | Package not installed | Run pip install claude-agent-sdk |
Invalid API key | API key missing or invalid | Check .env file has valid ANTHROPIC_API_KEY |
Command failed with exit code 1 | Various causes | Check logs, verify API key, check Claude CLI installation |
python3: command not found | Python not in PATH | Install Python 3.10+ or fix PATH |
venv creation failed | Missing python3-venv | sudo apt install python3-venv (Linux) |
| Agent timeout | Task too complex | Increase AGENT_TIMEOUT_SECONDS or simplify task |
| Logs directory growing | No cleanup | Manually clean logs/ directory |
| Permission denied errors | Restrictive settings.local.json | Add required commands to allow list |
Authentication
Set your API key in the .env file:
cp .env.example .env
ANTHROPIC_API_KEY=sk-ant-api03-xxxxx...
Verify the key works (from within existing venv):
curl -s https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4-5-20250929","max_tokens":10,"messages":[{"role":"user","content":"Hi"}]}'
Debugging Tips
- Check logs: Review
logs/agent_{timestamp}.log for detailed execution trace
- Verify API key: Run
echo $ANTHROPIC_API_KEY | head -c 15 (should show sk-ant-api...)
- Test SDK: Run
python -c "from claude_agent_sdk import query; print('OK')"
- Check permissions: Verify
.claude/settings.local.json allows required operations
- Inspect message history: Check
output/session_{session_id}.jsonl for full conversation trace
- Test CLI: Run
claude -p "hello" to verify Claude CLI works
Platform-Specific Notes
macOS/Linux:
- Use
source .venv/bin/activate
- Set env vars with
export VAR=value
- Load .env automatically with
python-dotenv
Windows:
- Use
.\.venv\Scripts\Activate.ps1 (PowerShell) or .venv\Scripts\activate.bat (CMD)
- Set env vars with
$env:VAR="value" (PowerShell) or set VAR=value (CMD)
- Path separators: use
\ in paths or raw strings
Docker:
- Mount
.env file: -v $(pwd)/.env:/app/.env
- Or set env directly:
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY
Converted and distributed by TomeVault — claim your Tome and manage your conversions.