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.mdfor agent's task - •Customize
.claude/settings.local.jsonpermissions 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
- •Run
- •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-codeorbrew install claude) - •
ANTHROPIC_API_KEYis set in.envfile
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 toconfigurationsarray (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:
bash
python3 -m venv .venv
- •Activate and install dependencies:
bash
source .venv/bin/activate # Unix/macOS 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
sudoor 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",
},
"cwd": "${workspaceFolder}/{agent-path}",
"envFile": "${workspaceFolder}/.env"
}
]
}
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__)
# ANSI color codes for verbose output
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": getattr(block, 'input', {}),
}
elif block_type == 'tool_result':
content = getattr(block, 'content', '')
return {
"type": "tool_result",
"is_error": getattr(block, 'is_error', False),
"content": str(content)[:500] if content else "",
}
elif block_type == 'text':
return {"type": "text", "text": getattr(block, 'text', '')}
return {"type": str(block_type), "raw": str(block)[:200]}
def print_verbose(prefix: str, content: str, color: str = RESET) -> None:
"""Print verbose output with colored prefix."""
print(f"{color}{BOLD}[{prefix}]{RESET} {content}", flush=True)
async def run_agent_with_timeout(
task: str,
working_dir: str = ".",
additional_dirs: Optional[list[str]] = None,
verbose: bool = True,
) -> AgentResult:
"""Run the agent with configuration from environment."""
max_turns = int(os.getenv("AGENT_MAX_TURNS", "50"))
timeout_seconds = int(os.getenv("AGENT_TIMEOUT_SECONDS", "1800"))
max_budget = float(os.getenv("AGENT_MAX_BUDGET_USD", "10.0"))
model = os.getenv("AGENT_MODEL", "claude-sonnet-4-5")
permission_mode = os.getenv("AGENT_PERMISSION_MODE", "bypassPermissions")
logger.info(f"Starting agent with model={model}, max_turns={max_turns}")
# add_dirs grants the agent read/write access to directories outside cwd.
# Use this when the agent needs to access external resources, configs, or data.
options = ClaudeAgentOptions(
model=model,
cwd=working_dir,
permission_mode=permission_mode,
max_turns=max_turns,
max_budget_usd=max_budget,
add_dirs=additional_dirs or [], # Additional allowed directories
)
try:
result_text = ""
session_id = ""
num_turns = 0
total_cost = None
message_history: list[dict[str, Any]] = []
async def run_query():
nonlocal result_text, session_id, num_turns, total_cost
turn_count = 0
async for message in query(prompt=task, options=options):
if isinstance(message, AssistantMessage):
turn_count += 1
content = getattr(message, 'message', None)
serialized_blocks: list[dict[str, Any]] = []
if content:
msg_content = getattr(content, 'content', None)
if msg_content:
for block in msg_content:
serialized_blocks.append(serialize_block(block))
if verbose:
block_type = getattr(block, 'type', None)
if block_type == 'text':
text = getattr(block, 'text', '')
if text:
preview = text[:500] + ("..." if len(text) > 500 else "")
print_verbose("ASSISTANT", preview, GREEN)
elif block_type == 'tool_use':
tool_name = getattr(block, 'name', 'unknown')
tool_input = getattr(block, 'input', {})
print_verbose("TOOL", f"{tool_name}", YELLOW)
if tool_name == 'Bash' and 'command' in tool_input:
print_verbose("CMD", tool_input['command'][:200], MAGENTA)
message_history.append({
"turn": turn_count,
"role": "assistant",
"timestamp": time.time(),
"content": serialized_blocks,
})
if verbose:
print_verbose(f"TURN {turn_count}", "", CYAN)
elif isinstance(message, UserMessage):
content = getattr(message, 'message', None)
if content:
msg_content = getattr(content, 'content', None)
if msg_content and verbose:
for block in msg_content:
block_type = getattr(block, 'type', None)
if block_type == 'tool_result':
result_content = getattr(block, 'content', '')
if isinstance(result_content, str) and result_content:
preview = result_content[:300] + ("..." if len(result_content) > 300 else "")
print_verbose("RESULT", preview, BLUE)
elif isinstance(message, SystemMessage):
if verbose:
print_verbose("SYSTEM", str(message)[:200], CYAN)
if isinstance(message, ResultMessage):
result_text = getattr(message, 'result', str(message))
session_id = getattr(message, 'session_id', '')
num_turns = getattr(message, 'num_turns', turn_count)
total_cost = getattr(message, 'total_cost_usd', None)
if verbose:
cost_str = f"${total_cost:.4f}" if total_cost else "N/A"
print_verbose("COMPLETE", f"Turns: {num_turns}, Cost: {cost_str}", GREEN)
await asyncio.wait_for(run_query(), timeout=timeout_seconds)
metrics = LLMMetrics(
model=model,
duration_ms=0,
num_turns=num_turns,
session_id=session_id,
total_cost_usd=total_cost,
)
return AgentResult(
status=TaskStatus.SUCCESS,
summary="Task completed successfully",
details=result_text,
metrics=metrics,
message_history=message_history,
)
except asyncio.TimeoutError:
raise AgentError(f"Agent timed out after {timeout_seconds} seconds")
except Exception as e:
error_msg = str(e)
if "max_turns" in error_msg.lower():
raise MaxTurnsExceededError(f"Exceeded {max_turns} turns")
if "server" in error_msg.lower() or "api" in error_msg.lower():
raise ServerError(f"API error: {error_msg}")
if "incomplete" in error_msg.lower():
raise SessionIncompleteError(f"Session incomplete: {error_msg}")
raise AgentError(f"Agent execution failed: {error_msg}")
claude_agent.py
#!/usr/bin/env python3
"""
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 environment variables from .env file
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 with colors
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",
},
))
# File handler for persistent logs
log_file = logs_dir / f"agent_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
file_handler = logging.FileHandler(log_file, encoding="utf-8")
file_handler.setFormatter(logging.Formatter(
"%(asctime)s [%(levelname)s] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
))
logger = colorlog.getLogger(__name__)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
logger.setLevel(logging.INFO)
return logger
def load_task_file(task_file: Path) -> str:
if not task_file.exists():
raise FileNotFoundError(f"Task file not found: {task_file}")
return task_file.read_text(encoding="utf-8")
def save_message_history(
result: AgentResult,
output_dir: Path,
session_id: str,
logger: logging.Logger,
) -> None:
"""Save message history to JSONL file."""
if not result.message_history:
return
output_dir.mkdir(parents=True, exist_ok=True)
output_file = output_dir / f"session_{session_id}.jsonl"
with output_file.open("w", encoding="utf-8") as f:
for msg in result.message_history:
f.write(json.dumps(msg) + "\n")
logger.info(f"Message history saved to: {output_file}")
def main() -> int:
base_dir = Path(__file__).parent
logs_dir = base_dir / "logs"
output_dir = base_dir / "output"
logger = setup_logging(logs_dir)
parser = argparse.ArgumentParser(description="Execute Claude agent task")
parser.add_argument(
"--dir",
default=".",
help="Working directory (default: current)",
)
parser.add_argument(
"--task-file",
type=Path,
default=Path("task.md"),
help="Task file (default: task.md)",
)
parser.add_argument(
"--output",
help="Output file path (default: stdout)",
)
parser.add_argument(
"--add-dir",
action="append",
default=[],
help="Additional directories",
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Enable verbose output",
)
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
try:
task: str = load_task_file(args.task_file)
except FileNotFoundError as e:
logger.error(str(e))
return 1
logger.info(f"Task file: {args.task_file}")
logger.info(f"Working directory: {args.dir}")
try:
result = asyncio.run(
run_agent_with_timeout(
task=task,
working_dir=args.dir,
additional_dirs=args.add_dir,
verbose=args.verbose,
)
)
# Save message history to JSONL
session_id = (
result.metrics.session_id
if result.metrics
else datetime.now().strftime("%Y%m%d_%H%M%S")
)
save_message_history(result, output_dir, session_id, logger)
# Output result JSON (excluding message_history to avoid duplication)
output_json: str = result.model_dump_json(
indent=2, exclude={"message_history"}
)
if args.output:
Path(args.output).write_text(output_json, encoding="utf-8")
logger.info(f"Result saved to: {args.output}")
else:
print(output_json)
if result.metrics:
logger.info(
f"Turns: {result.metrics.num_turns}, "
f"Cost: ${result.metrics.total_cost_usd:.4f}"
if result.metrics.total_cost_usd
else f"Turns: {result.metrics.num_turns}"
)
return 0 if result.status == TaskStatus.SUCCESS else 1
except Exception as e:
logger.error(f"Agent failed: {e}")
return 1
if __name__ == "__main__":
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:
# Copy example and edit cp .env.example .env # Add your key to .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}.logfor detailed execution trace - •Verify API key: Run
echo $ANTHROPIC_API_KEY | head -c 15(should showsk-ant-api...) - •Test SDK: Run
python -c "from claude_agent_sdk import query; print('OK')" - •Check permissions: Verify
.claude/settings.local.jsonallows required operations - •Inspect message history: Check
output/session_{session_id}.jsonlfor 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) orset VAR=value(CMD) - •Path separators: use
\in paths or raw strings
Docker:
- •Mount
.envfile:-v $(pwd)/.env:/app/.env - •Or set env directly:
-e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY