1. Built-In Tools Reference
These tools are available to every Agent SDK query by default. You don’t implement them — you control access via allowed_tools and disallowed_tools. Each tool has a fixed schema and behavior provided by the SDK.
1.1 File Tools (Read, Edit, Write)
| Tool | Purpose | Requires Permission? | Key Input |
|---|---|---|---|
Read | Read file contents (full or line range) | No (read-only) | file_path, start_line, end_line |
Edit | Surgical edit: find & replace text in a file | Yes (modifies files) | file_path, old_string, new_string |
Write | Create new file or overwrite entirely | Yes (creates/overwrites) | file_path, content |
# File Tools — Usage Patterns
# Requires: pip install claude-agent-sdk
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
async def file_operations_demo():
"""Demonstrate file tool usage with proper permissions."""
async for message in query(
prompt="Read src/config.py, then add a DATABASE_URL environment variable.",
options=ClaudeAgentOptions(
# Read is always allowed. Edit/Write require permission_mode.
allowed_tools=["Read", "Edit", "Write"],
# acceptEdits: auto-approve file modifications (no interactive prompt)
permission_mode="acceptEdits",
),
):
if isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "name"):
print(f" Tool: {block.name} → {block.input.get('file_path', '')}")
if isinstance(message, ResultMessage) and message.subtype == "success":
print(f"\nResult: {message.result[:200]}")
asyncio.run(file_operations_demo())
Edit for modifying existing files. It makes surgical find-and-replace changes and preserves the rest of the file. Write replaces the entire file content and is only appropriate for creating new files or when the entire file needs rewriting.
1.2 Search Tools (Glob, Grep)
| Tool | Purpose | Requires Permission? | Key Input |
|---|---|---|---|
Glob | Find files by path pattern | No (read-only) | pattern (e.g., **/*.py) |
Grep | Search file contents by regex | No (read-only) | pattern, path (scope) |
# Search Tools — Finding Code Efficiently
# Best practice: Glob → Grep → Read → Edit (incremental exploration)
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def search_demo():
async for message in query(
prompt="Find all files that import 'jwt' and show where tokens are validated.",
options=ClaudeAgentOptions(
# Read-only tools — no permission needed
allowed_tools=["Glob", "Grep", "Read"],
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(search_demo())
1.3 Execution Tools (Bash)
| Tool | Purpose | Requires Permission? | Key Input |
|---|---|---|---|
Bash | Run shell commands | Yes (executes code) | command, timeout |
# Bash Tool — Running Shell Commands
# CRITICAL: Bash is the most powerful (and dangerous) built-in tool
# Always use scoped rules (Section 3.2) to restrict what it can run
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def bash_demo():
async for message in query(
prompt="Run the test suite and report any failures.",
options=ClaudeAgentOptions(
allowed_tools=["Bash(pytest *)", "Read"],
# Scoped allow rules auto-approve only the commands you list.
# acceptEdits does NOT auto-approve arbitrary Bash commands.
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(bash_demo())
1.4 Web Tools (WebSearch, WebFetch)
| Tool | Purpose | Requires Permission? | Key Input |
|---|---|---|---|
WebSearch | Search the web (returns summaries) | No (read-only) | query |
WebFetch | Fetch a specific URL’s content | No (read-only) | url |
# Web Tools — Internet Access for Research
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def web_research():
async for message in query(
prompt="Research the latest Python 3.13 features and summarize the key changes.",
options=ClaudeAgentOptions(
allowed_tools=["WebSearch", "WebFetch"],
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result)
asyncio.run(web_research())
1.5 Meta Tools (Agent, Skill, AskUserQuestion)
| Tool | Purpose | Requires Permission? | Key Input |
|---|---|---|---|
Agent | Spawn subagent with isolated context | No | subagent_type, prompt |
Skill | Invoke discovered filesystem or plugin skills | Depends on the tools the skill uses | skill_name, arguments |
AskUserQuestion | Request input from the user | No | question |
# Meta Tools — Agent Orchestration
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ResultMessage
async def orchestration_demo():
"""Agent spawns subagents; skills are discovered from the filesystem."""
async for message in query(
prompt="Research Rust async patterns, then write a comparison with Python.",
options=ClaudeAgentOptions(
# Agent tool enables subagent delegation (Part 4)
# Skills become available through the skills setting, not allowed_tools
allowed_tools=["Agent", "WebSearch", "Read", "Write"],
skills="all",
agents={
"web-researcher": AgentDefinition(
description="Search the web for technical information.",
prompt="You are a research specialist. Find accurate, current information.",
tools=["WebSearch", "WebFetch"],
model="sonnet",
),
},
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result[:300])
asyncio.run(orchestration_demo())
allowed_tools. It runs in the background and is configured via the ENABLE_TOOL_SEARCH environment variable.
1B. Server-Executed Tools (API-Level)
The tools in Section 1 are client-executed — Claude decides to call them, and your code runs the operation. Server-executed tools are different: Anthropic’s infrastructure runs them internally. You enable the tool in your request and receive both the tool call and its result in the response — no tool_result round-trip needed.
tool_use blocks (you respond with tool_result). Server tools use server_tool_use blocks (result is already included, prefixed with srvtoolu_). You never send a tool_result for server tools.
1B.1 The server_tool_use Block
import anthropic
import json
client = anthropic.Anthropic()
# Server tools: enable in the tools array, Anthropic handles execution
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[
# Web search — server-executed (type prefix identifies server tools)
{"type": "web_search_20250305", "name": "web_search", "max_uses": 5},
],
messages=[{"role": "user", "content": "What's the latest news about quantum computing?"}]
)
# Response contains BOTH the tool call AND its result
for block in response.content:
if block.type == "server_tool_use":
# id prefix: srvtoolu_ (not toolu_)
print(f"Server tool called: {block.name}")
print(f"ID: {block.id}") # srvtoolu_01A2B3C4...
print(f"Input: {json.dumps(block.input, indent=2)}")
elif block.type == "web_search_tool_result":
# Result arrives in the SAME response — no round-trip needed
print(f"Results: {len(block.content)} search results returned")
for result in block.content:
if hasattr(result, 'url'):
print(f" - {result.title}: {result.url}")
elif block.type == "text":
# Claude's synthesis of search results (may include citations)
print(f"Response: {block.text[:200]}")
if hasattr(block, 'citations') and block.citations:
print(f" Citations: {len(block.citations)} sources cited")
# Usage shows server tool consumption
print(f"\nServer tool usage: {response.usage.server_tool_use}")
# {"web_search_requests": 1}
1B.2 The pause_turn Stop Reason
Server tools run their own internal loop. If the model is still iterating when it hits the iteration cap, the response returns with stop_reason: "pause_turn" instead of "end_turn". You must continue the conversation to let the model finish:
import anthropic
client = anthropic.Anthropic()
# Complex query that may require multiple search iterations
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
messages=[{
"role": "user",
"content": "Research and compare the top 5 quantum computing companies by funding, technology approach, and recent breakthroughs."
}]
)
# Handle pause_turn — continue the conversation
while response.stop_reason == "pause_turn":
print(f"[Paused — continuing turn...]")
# Pass the paused response back as-is
messages = [
{
"role": "user",
"content": "Research and compare the top 5 quantum computing companies..."
},
{"role": "assistant", "content": response.content}, # Include paused content
]
# Continue — same tools, same context
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 10}],
messages=messages,
)
# Final response
print(f"Stop reason: {response.stop_reason}")
for block in response.content:
if block.type == "text":
print(block.text[:500])
1B.3 Web Search Tool — Advanced Features
The web search tool (web_search_20250305 for basic, web_search_20260209 for dynamic filtering) gives Claude real-time web access with citations. Key features beyond basic search:
import anthropic
import json
client = anthropic.Anthropic()
# === DOMAIN FILTERING — restrict which sites Claude can search ===
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3,
# Only search these domains (subdomains included automatically)
"allowed_domains": ["docs.python.org", "realpython.com", "stackoverflow.com"],
# OR block specific domains:
# "blocked_domains": ["reddit.com", "quora.com"],
# Cannot use both allowed_domains AND blocked_domains in same request
}],
messages=[{"role": "user", "content": "How do I use asyncio.gather in Python?"}]
)
# === LOCALIZATION — get region-specific results ===
response_local = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"type": "web_search_20250305",
"name": "web_search",
"user_location": {
"type": "approximate",
"city": "London",
"region": "England",
"country": "GB",
"timezone": "Europe/London"
}
}],
messages=[{"role": "user", "content": "What restaurants are open near me?"}]
)
# === DYNAMIC FILTERING (20260209 version) — code-based result filtering ===
# Requires code_execution to be enabled implicitly
response_dynamic = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[
# 20260209 uses code to filter results BEFORE loading into context
{"type": "web_search_20260209", "name": "web_search"},
],
messages=[{
"role": "user",
"content": "Search for Python asyncio best practices and filter for articles from 2025 or later"
}]
)
# === CITATIONS — always returned with web search ===
for block in response.content:
if block.type == "text" and hasattr(block, 'citations') and block.citations:
for citation in block.citations:
print(f" Source: {citation.title}")
print(f" URL: {citation.url}")
print(f" Quoted: {citation.cited_text[:80]}...")
# cited_text, title, url do NOT count toward token usage
# === PRICING ===
# $10 per 1,000 searches + standard token costs
# Each search = one use regardless of result count
# Errors are not billed
print(f"Searches performed: {response.usage.server_tool_use.get('web_search_requests', 0)}")
1B.4 Code Execution Tool
The code execution tool (code_execution_20250522) runs Python in a sandboxed container on Anthropic’s servers. Claude can write and execute code, read outputs, and iterate — all server-side with no client execution required:
import anthropic
import json
client = anthropic.Anthropic()
# Enable server-side code execution
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[
{"type": "code_execution_20250522", "name": "code_execution"}
],
messages=[{
"role": "user",
"content": "Calculate the first 20 Fibonacci numbers and plot them on a chart. Show the growth rate."
}]
)
# Response contains code execution blocks
for block in response.content:
if block.type == "server_tool_use" and block.name == "code_execution":
print(f"Code executed: {block.input.get('code', '')[:100]}...")
elif block.type == "code_execution_tool_result":
# Contains stdout, stderr, and any generated files/images
print(f"Execution result available")
if hasattr(block, 'content'):
for item in block.content:
if hasattr(item, 'text'):
print(f" Output: {item.text[:200]}")
elif block.type == "text":
print(f"Claude: {block.text[:300]}")
# Use cases for code execution:
# - Data analysis and visualization
# - Mathematical calculations
# - Code validation and testing
# - File format conversion
# - Dynamic filtering for web search (20260209 version uses this internally)
print(f"\nStop reason: {response.stop_reason}")
1B.5 Tool Search (Server Tool)
When you have hundreds of tools, sending all definitions on every request wastes context. The tool_search server tool lets Claude discover and load tools on demand from a large catalog:
import anthropic
import json
client = anthropic.Anthropic()
# Tool search: Claude queries a tool catalog and loads what it needs
# Define a large set of tools but mark most as "deferred" (not sent initially)
all_tools = [
# Server tool: enables tool discovery
{"type": "tool_search_20250520", "name": "tool_search"},
# Only frequently-used tools are sent in full
{
"name": "get_customer",
"description": "Look up a customer by ID or email.",
"input_schema": {
"type": "object",
"properties": {"identifier": {"type": "string"}},
"required": ["identifier"]
}
},
# Deferred tools: schema NOT sent initially — loaded via tool_search
{
"name": "generate_invoice",
"description": "Generate a PDF invoice for an order.",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"format": {"type": "string", "enum": ["pdf", "html"]}
},
"required": ["order_id"]
},
"defer_loading": True # Not sent until tool_search discovers it
},
{
"name": "schedule_appointment",
"description": "Schedule a customer appointment.",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {"type": "string"},
"datetime": {"type": "string"},
"type": {"type": "string", "enum": ["support", "sales", "onboarding"]}
},
"required": ["customer_id", "datetime", "type"]
},
"defer_loading": True
}
]
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=all_tools,
messages=[{
"role": "user",
"content": "I need to schedule a support appointment for customer C-1234 tomorrow at 2pm."
}]
)
# Claude uses tool_search to find schedule_appointment, then calls it
for block in response.content:
if block.type == "server_tool_use" and block.name == "tool_search":
print(f"Tool search query: {block.input}")
elif block.type == "tool_use":
print(f"Tool called: {block.name} with {json.dumps(block.input)}")
elif block.type == "text":
print(f"Response: {block.text[:200]}")
# Benefits:
# - Reduces input tokens (deferred tools not sent in system prompt)
# - Scales to 100+ tools without context window bloat
# - Claude discovers tools semantically (by description match)
# - Use namespaced tool names for better search: github_list_prs, slack_send_message
1B.6 Anthropic-Schema Client Tools (Memory, Computer, Text Editor)
These tools have Anthropic-published schemas but run on your infrastructure (client-executed). Claude is trained on thousands of successful trajectories with these exact schemas, so it calls them more reliably than equivalent custom tools:
import anthropic
import json
client = anthropic.Anthropic()
# === MEMORY TOOL — Claude's scratchpad for long conversations ===
# Type: "memory_20250520" — client-executed but Anthropic-schema
memory_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"type": "memory_20250520",
"name": "memory"
# Your code handles the read/write operations
}],
messages=[{"role": "user", "content": "Remember that my preferred language is Python."}]
)
# Claude emits: tool_use with name="memory", input={"action": "write", "content": "..."}
# You implement: store to your persistence layer, return confirmation
# === TEXT EDITOR TOOL — Surgical file editing ===
# Type: "text_editor_20250429" — for line-level file modifications
editor_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
tools=[{
"type": "text_editor_20250429",
"name": "text_editor"
}],
messages=[{"role": "user", "content": "Change the database timeout from 30 to 60 seconds in config.py"}]
)
# Claude emits precise edit commands — your code applies them to the filesystem
# === COMPUTER USE TOOL — GUI automation ===
# Type: "computer_20250124" — controls mouse, keyboard, screenshots
computer_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
tools=[{
"type": "computer_20250124",
"name": "computer",
"display_width_px": 1920,
"display_height_px": 1080,
"display_number": 1
}],
messages=[{"role": "user", "content": "Open the browser and navigate to example.com"}]
)
# Claude emits: mouse_move, click, type, screenshot actions
# Your code: drives the actual desktop (via pyautogui, Playwright, etc.)
# === BASH TOOL (Anthropic-schema) ===
# Type: "bash_20250124" — shell command execution
bash_response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
tools=[{
"type": "bash_20250124",
"name": "bash"
}],
messages=[{"role": "user", "content": "List all Python files in the current directory"}]
)
# Claude emits: {"command": "find . -name '*.py'"} — your code runs it
# Key insight: these are the SAME tools Claude Code uses internally.
# The schemas are "trained-in" — Claude calls them with higher accuracy
# than equivalent custom tools with the same functionality.
print("Anthropic-schema tools: you handle execution, Claude knows the schema natively")
print("Server tools: Anthropic handles execution, you just enable them")
- Server tools (web_search, web_fetch, code_execution, tool_search): You enable them, Anthropic executes them. Look for
server_tool_useblocks withsrvtoolu_IDs. - Anthropic-schema client tools (bash, text_editor, computer, memory): You execute them, but Claude knows the schema natively. Look for regular
tool_useblocks. - User-defined client tools: You define the schema AND execute. Maximum flexibility, requires more code.
2. Permission Modes
Permission modes control how the SDK handles tool calls that modify the environment (file edits, shell commands). In interactive mode, the user sees a prompt and can approve/deny. In automated mode, you pre-configure the behavior.
2.1 Default Mode
Prompts the user for approval on every potentially destructive action. Ideal for interactive development sessions:
# Default Mode — Interactive Approval for Every Write/Bash
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def default_mode():
"""User is prompted to approve each Edit/Write/Bash call."""
async for message in query(
prompt="Refactor the auth module to use async/await.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
# permission_mode="default" is implied when not specified
# Each Edit or Bash call pauses and asks "Allow this?" in the terminal
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}] Cost: ${message.total_cost_usd:.4f}")
asyncio.run(default_mode())
2.2 acceptEdits Mode
Auto-approves file modifications (Edit, Write) but still prompts for Bash commands. Best for trusted code generation tasks where you review the diff afterward:
# acceptEdits — Auto-approve file changes, still ask for Bash
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def accept_edits_mode():
"""File edits run freely. Bash commands still require approval."""
async for message in query(
prompt="Add type hints to all functions in src/utils.py",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob", "Grep"],
permission_mode="acceptEdits",
# Edit and Write: ✅ auto-approved
# Bash: ⚠️ still prompts (could run dangerous commands)
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}]")
asyncio.run(accept_edits_mode())
2.3 Plan Mode
Read-only mode — the agent can explore the codebase but cannot modify anything. Returns a plan of proposed changes without executing them:
# Plan Mode — Read-only exploration, no modifications
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def plan_mode():
"""Agent reads and plans. No Edit/Write/Bash allowed."""
async for message in query(
prompt="What changes would be needed to migrate from Flask to FastAPI?",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"], # Read-only tools
permission_mode="plan",
# Blocks ALL mutations. Even if you include Edit in allowed_tools,
# plan mode overrides and denies.
),
):
if isinstance(message, ResultMessage) and message.subtype == "success":
print(message.result) # Returns detailed plan, no files modified
asyncio.run(plan_mode())
2.4 dontAsk / bypassPermissions
Full automation modes — no interactive prompts at all. Use for CI/CD pipelines and automated scripts where no user is present:
# bypassPermissions — Full automation (CI/CD, headless)
# WARNING: The agent can run ANY allowed tool without asking. Use only in controlled environments.
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def ci_mode():
"""Fully automated — no prompts, no approval. For CI/CD only."""
async for message in query(
prompt="Run the test suite, fix any failing tests, then commit the fixes.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
permission_mode="bypassPermissions",
# EVERY tool call auto-approved. No user interaction.
# Combine with max_budget_usd to prevent runaway costs:
max_budget_usd=1.00,
max_turns=50,
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}] Cost: ${message.total_cost_usd:.4f}")
asyncio.run(ci_mode())
bypassPermissions should ONLY be used in sandboxed environments (Docker, CI runners) where the agent cannot cause permanent damage. Always pair it with max_budget_usd and max_turns to prevent runaway execution.
3. Permission Evaluation Order
When the agent calls a tool, the SDK evaluates whether to allow it through a fixed sequence of checks. Understanding this order is essential for debugging “why was this tool blocked?” issues:
3.1 Full Evaluation Flow
flowchart TD
T["Tool Call"] --> H{"1. Hooks
(PreToolUse)"}
H -->|"deny"| DENY["❌ DENIED"]
H -->|"allow or {}"| D{"2. disallowed_tools
list?"}
D -->|"tool in list"| DENY
D -->|"not in list"| M{"3. permission_mode
check"}
M -->|"plan mode + write tool"| DENY
M -->|"dontAsk/bypass"| ALLOW
M -->|"needs approval"| A{"4. allowed_tools
list?"}
A -->|"tool in list"| ALLOW
A -->|"not in list"| C{"5. canUseTool
callback?"}
C -->|"returns allow"| ALLOW
C -->|"returns deny"| DENY
C -->|"no callback"| P["6. Prompt User"]
# Permission Evaluation Order — Demonstrate with Comments
# Requires: pip install claude-agent-sdk
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, HookMatcher, ResultMessage
async def protect_production_db(input_data, tool_use_id, context):
"""Step 1: hooks fire first. deny blocks immediately; allow still flows onward."""
command = input_data.get("tool_input", {}).get("command", "")
if "DROP TABLE" in command.upper() or "production" in command:
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Production database access blocked by hook",
}
}
return {}
async def evaluation_demo():
"""Shows how the evaluation chain works."""
async for message in query(
prompt="Check the database schema in the production DB.",
options=ClaudeAgentOptions(
# Step 2: disallowed_tools — hard block (cannot override)
disallowed_tools=["Write"], # Never allow full file overwrites
# Step 3: permission_mode — default behavior
permission_mode="acceptEdits", # Auto-approve Edit, prompt for Bash
# Step 4: allowed_tools — pre-approved list
allowed_tools=[
"Read", "Edit", "Grep", "Glob",
"Bash(npm test)", # Scoped: only 'npm test' auto-approved
"Bash(git status)", # Scoped: only 'git status' auto-approved
],
# Step 1: Hooks run first. A deny blocks immediately.
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[protect_production_db])
],
},
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}]")
asyncio.run(evaluation_demo())
3.2 Scoped Rules (Bash Patterns)
Scoped rules let you approve specific command patterns without blanket-approving all Bash commands. The syntax is Bash(pattern) where pattern is matched against the command:
# Scoped Bash Rules — Fine-Grained Command Approval
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def scoped_bash_demo():
"""Only specific Bash commands are auto-approved."""
async for message in query(
prompt="Install the project dependencies and run the linter.",
options=ClaudeAgentOptions(
allowed_tools=[
"Read", "Edit", "Glob", "Grep",
# Scoped Bash rules — only these patterns auto-approve:
"Bash(npm install)", # Exact match
"Bash(npm run lint)", # Exact match
"Bash(npm test)", # Exact match
"Bash(git *)", # Wildcard: any git command
"Bash(python -m pytest *)", # Wildcard: pytest with any args
# These Bash commands would still require user approval:
# - "rm -rf ..." (not in allowed list)
# - "curl ... | bash" (not in allowed list)
# - "sudo ..." (not in allowed list)
],
permission_mode="acceptEdits", # File edits auto-approved
),
):
if isinstance(message, ResultMessage):
print(f"Done: {message.subtype}")
asyncio.run(scoped_bash_demo())
Bash(git *) matches “git status”, “git commit -m ‘fix’”, “git push origin main”, etc. Use * for wildcard portions. Without a wildcard, it’s an exact match. The pattern is matched against the full command string the agent wants to run.
4. allowed_tools & disallowed_tools
4.1 Patterns & Wildcards
# allowed_tools and disallowed_tools — Complete Pattern Reference
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def tool_access_patterns():
async for message in query(
prompt="Analyze the project and suggest improvements.",
options=ClaudeAgentOptions(
# ALLOWED: tools the agent CAN use (whitelist)
allowed_tools=[
# Built-in tools (by name)
"Read",
"Glob",
"Grep",
# MCP tools (by server + tool name)
"mcp__github__list_issues",
"mcp__github__get_issue",
# MCP wildcard (all tools from a server)
"mcp__analytics__*",
# Scoped Bash (specific commands only)
"Bash(npm test)",
"Bash(git *)",
],
# DISALLOWED: tools the agent CANNOT use (blocklist, overrides allowed)
# disallowed_tools takes precedence over allowed_tools
disallowed_tools=[
"Write", # Never overwrite files (use Edit)
"mcp__github__delete_repo", # Block dangerous GitHub operations
],
),
):
if isinstance(message, ResultMessage):
print(f"Result: {message.subtype}")
asyncio.run(tool_access_patterns())
disallowed_tools always wins over allowed_tools. If a tool appears in both lists, it is blocked. This ensures safety rules cannot be accidentally overridden.
4.2 Restricting Subagent Tools
When you define subagents via AgentDefinition (Part 4), each subagent gets its own tool list. This creates a security boundary — a research subagent should never have Edit or Bash access:
# Subagent Tool Restrictions — Security Boundaries
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition, ResultMessage
async def restricted_subagents():
async for message in query(
prompt="Research cloud pricing, then update our cost comparison doc.",
options=ClaudeAgentOptions(
# Coordinator has full access
allowed_tools=["Agent", "Read", "Edit", "Glob", "WebSearch"],
permission_mode="acceptEdits",
agents={
# Research subagent: web-only, no file access
"researcher": AgentDefinition(
description="Research web content for cloud pricing data.",
prompt="Find current pricing for AWS, GCP, and Azure compute instances.",
tools=["WebSearch", "WebFetch"], # Read-only, web-only
# Subagent CANNOT use Edit, Write, Bash, or Read
),
# Writer subagent: file access but no web or shell
"doc-writer": AgentDefinition(
description="Update documentation files with provided content.",
prompt="Update markdown files with the latest pricing data.",
tools=["Read", "Edit"], # Can edit files but nothing else
# disallowed in subagent: Bash, Write, WebSearch
),
},
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}] Cost: ${message.total_cost_usd:.4f}")
asyncio.run(restricted_subagents())
5. canUseTool Callback (Runtime Approval)
For cases where static rules aren’t enough, implement a canUseTool callback. This function runs at step 5 of the evaluation order (after hooks, disallowed, mode, and allowed checks) and lets you make dynamic decisions based on the actual tool input:
# canUseTool — Dynamic Runtime Approval
# Use when: approval depends on the CONTENT of the tool call, not just the tool name
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def can_use_tool_callback(tool_name: str, tool_input: dict) -> str:
"""
Called when a tool isn't in allowed_tools and isn't blocked.
Returns: "allow", "deny", or "ask" (prompt user interactively).
"""
# Example: allow Bash only for commands under 50 chars (simple commands)
if tool_name == "Bash":
command = tool_input.get("command", "")
if len(command) > 50:
return "deny" # Block complex commands
if command.startswith("rm"):
return "deny" # Never allow rm
return "allow" # Simple commands OK
# Example: allow Edit only for non-production files
if tool_name == "Edit":
file_path = tool_input.get("file_path", "")
if "/production/" in file_path or "/prod/" in file_path:
return "deny" # Block production file edits
return "allow"
return "ask" # Fallback: prompt user for anything else
async def dynamic_approval():
async for message in query(
prompt="Fix the failing unit tests in the project.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"], # Read tools pre-approved
# canUseTool is called for Edit and Bash (not in allowed_tools)
can_use_tool=can_use_tool_callback,
),
):
if isinstance(message, ResultMessage):
print(f"[{message.subtype}] Turns: {message.num_turns}")
asyncio.run(dynamic_approval())
Permission System in CI/CD
A deployment pipeline uses bypassPermissions + max_budget_usd: 2.00 + scoped Bash rules ["Bash(npm *)", "Bash(git *)"]. A PreToolUse hook blocks any Bash command containing sudo, rm -rf, or curl | bash. The canUseTool callback logs all tool calls to an audit database. This layered approach gives automation freedom while maintaining security boundaries.
disallowed_tools for hard blocks, (2) permission mode for default behavior, (3) allowed_tools with scoped rules for fine-grained whitelist, (4) PreToolUse hooks for content-based enforcement, (5) canUseTool for dynamic approval logic. Each layer catches what the previous one missed.
Next in the SDK Track
In Part 23: Sessions, Streaming & Deployment, we cover production patterns — session persistence across hosts, real-time streaming UIs, structured output schemas, file checkpointing, cost tracking, OpenTelemetry observability, and hosting the Agent SDK in Docker/serverless environments.
6. Skills: allowed-tools Frontmatter & /v1/skills API
Skills have their own permission layer built into the SKILL.md frontmatter: the allowed-tools field. When a skill is invoked, it enforces its own tool restrictions on top of the parent agent’s allowed_tools. The effective set is the intersection — a skill cannot grant tools the parent doesn’t allow.
---
name: code-reviewer
description: >
Reviews code for security vulnerabilities, style issues, and bugs.
Use when user asks to "review this", "check this code", or "code review".
# allowed-tools in SKILL.md = tool restrictions when this skill runs
# These restrict WITHIN the parent agent's allowed_tools — cannot grant extra access
allowed-tools: "Read Grep Glob" # Read-only: cannot Edit, Write, or Bash
metadata:
author: Wasil Zafar
version: 1.0.0
---
## Code Review Instructions
...
This principle maps directly to subagent tool restriction from Section 4.2: a code-reviewer skill that only has Read Grep Glob access cannot accidentally modify files, even if the calling agent has Edit and Write in its allowed_tools.
Skills API — programmatic skill management:
# /v1/skills — Manage skills via the Managed Agents API
# Skills require the Code Execution Tool beta to run securely
# Requires: pip install anthropic
from anthropic import Anthropic
client = Anthropic()
# --- LIST available skills ---
# GET /v1/skills
page = client.beta.skills.list()
for skill in page:
print(f" {skill.skill_id}: {skill.type} v{skill.version}")
# Anthropic-managed skill IDs include: "xlsx", "docx", "pptx", "pdf", etc.
# --- Use a skill in an agent ---
agent = client.beta.agents.create(
model="claude-sonnet-4-6",
name="document-agent",
system="You create professional documents on demand.",
skills=[
# Anthropic-managed skill (built-in, no upload needed)
{"skill_id": "xlsx", "type": "anthropic", "version": "1"},
# Custom skill (uploaded by you, see below)
{"skill_id": "my-custom-skill-id", "type": "custom", "version": "2"},
],
)
print(f"Agent with skills: {agent.id}")
# --- Add skills to a one-off Messages API request ---
# (container.skills — no persistent agent needed)
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
messages=[{"role": "user", "content": "Create an Excel report of Q1 sales data"}],
# container.skills requires Code Execution Tool beta
extra_headers={"anthropic-beta": "computer-use-2024-10-22"},
extra_body={"container": {"skills": ["xlsx"]}},
)
print(response.content[0].text[:200])
allowed_tools — Key Distinction:
allowed_toolsinClaudeAgentOptions/client.beta.agents.create(tools=[...])— controls what the agent can doallowed-toolsinSKILL.mdfrontmatter — controls what that skill can do when invoked- Skills cannot expand the agent’s permissions — only narrow them
- Anthropic-managed skills (xlsx, docx, etc.) come with pre-defined tool sets optimized for their task