Enabling File Edit, Bash, and Web Tools in the Agent SDK
The Claude Agent SDK ships four built-in tool families out of the box: file editing, bash execution, web search, and web fetch.
Search across all documentation pages
The Claude Agent SDK ships four built-in tool families out of the box: file editing, bash execution, web search, and web fetch.
None of them are on by default in an unscoped way; you choose which ones an agent can reach, and often how far each one can reach, before you ever call query().
Built-in tools are what let the tool-use loop actually change files, run commands, or pull in information from the web instead of only producing text.
Each tool family has its own configuration surface, not just an on/off switch, so "enabling bash" and "enabling unrestricted bash" are two different decisions.
Getting this right matters because every tool you enable is something the loop can invoke on its own, subject only to whatever permission mode and checkpoints you've layered on top.
This page covers how to enable and scope each built-in tool family, and how they interact with the SDK's permission modes.
Quick-reference recipe card - copy-paste ready.
from claude_agent_sdk import query, AgentOptions
options = AgentOptions(
allowed_tools=["file_edit", "bash", "web_search", "web_fetch"],
cwd="/path/to/project",
permission_mode="default",
)
async for message in query(
prompt="Update the changelog and verify the tests still pass.",
options=options,
):
print(message)When to reach for this:
permission_mode as a safety net.cwd.import asyncio
from claude_agent_sdk import query, AgentOptions
async def run_release_notes_agent(repo_path: str) -> None:
options = AgentOptions(
cwd=repo_path,
allowed_tools=["bash", "file_edit", "web_fetch"],
# bash is scoped to read-only inspection commands the agent needs
# to gather context; it is not given web_search since the task
# only needs to fetch one known URL, not search the open web.
tool_config={
"bash": {"allowed_commands": ["git log", "git diff", "git status"]},
},
permission_mode="default",
)
prompt = (
"Read the git log since the last tag, fetch the linked issue "
"for each commit from https://api.example.com/issues/{id}, and "
"write a RELEASE_NOTES.md summarizing the changes."
)
async for message in query(prompt=prompt, options=options):
if message.get("type") == "text":
print(message["text"], end="", flush=True)
elif message.get("type") == "tool_call":
print(f"\n[tool call: {message['tool_name']}]")
asyncio.run(run_release_notes_agent("/home/dev/my-project"))What this demonstrates:
bash, file_edit, web_fetch) for a task that genuinely needs to read history, fetch external data, and write a file.allowed_commands instead of granting unrestricted shell access.web_search because the task only needs to fetch known URLs, not search the web.allowed_tools is evaluated before the loop's decide step; a tool name that isn't in the list is invisible to the model, not merely blocked at execution time.file_edit covers reading and writing files under cwd; some SDK versions let you further restrict it to specific paths or globs via tool_config.bash executes shell commands in the working directory; scoping it to an allowed_commands list (or a denylist, depending on SDK version) limits it to specific command prefixes rather than an open shell.web_search queries a search index and returns results the model can reason over; web_fetch retrieves the content of one specific URL. They are separate tools because "search the web" and "fetch this one page" have different risk and cost profiles.permission_mode operates independently of allowed_tools: scoping decides what's reachable at all, permission mode decides whether a reachable, allowed call still needs a human to approve it first.| Tool | What it does | Common scoping knob |
|---|---|---|
file_edit | Read/write files under cwd | Path or glob restrictions |
bash | Execute shell commands | Allowed/denied command prefixes |
web_search | Query a search index | Result count, domain restrictions |
web_fetch | Retrieve one URL's content | Domain allowlist |
# Scoping file_edit to a subdirectory keeps a large monorepo agent
# from touching files outside the package it was asked to work on.
options = AgentOptions(
cwd="/repo",
allowed_tools=["file_edit"],
tool_config={
"file_edit": {"allowed_paths": ["packages/billing/**"]},
},
)| Parameter | Type | Description |
|---|---|---|
allowed_tools | list[str] | Allowlist of built-in tool names the loop may call |
cwd | str | Working directory root for file_edit and bash |
tool_config | dict | Per-tool scoping options (allowed commands, paths, domains) |
permission_mode | str | Whether allowed calls still need human approval (default, bypass, etc.) |
bash entry in allowed_tools can mean full shell access on some configurations. Fix: always pair bash with an explicit allowed_commands or equivalent restriction unless you truly need arbitrary shell access.web_search with web_fetch. Enabling web_search when the task only needs to read one known URL gives the agent a much larger, less predictable capability than it needs. Fix: enable web_fetch alone for known-URL tasks; reserve web_search for open-ended research.cwd in multi-project environments. Without an explicit cwd, file_edit and bash default to the process's working directory, which can differ between local runs and deployed environments. Fix: always set cwd explicitly rather than relying on the ambient directory.permission_mode as a substitute for scoping. A broad allowed_tools list with approval gates only on "obviously destructive" calls still leaves many actions unreviewed. Fix: scope tools down first; use permission mode as a second layer, not the only layer.file_edit to a read-only reporting agent. If a task never needs to write files, including file_edit anyway is unnecessary surface area. Fix: grant only the tools the task's actual outputs require.bash commands ahead of time. An allowed_commands list that's too strict silently breaks the agent mid-task with a permission error the model has to work around. Fix: dry-run the exact commands you expect the agent to need before locking down the allowlist.| Alternative | Use When | Don't Use When |
|---|---|---|
| Grant all built-in tools, rely on checkpoints | Prototyping locally with a human watching every step | Running unattended or against production data |
| MCP server as the only external tool | The task needs a specific external system, not general file/bash/web access | The task genuinely needs to edit local files or run shell commands |
| No tools, plain generation | The task is pure text generation with no need to act on anything | The task requires reading, writing, or fetching real data |
No. Enable only what the task needs; allowed_tools accepts any subset, and an empty or minimal list is valid for pure-text tasks.
The tool is not exposed to the model in the first place, so it cannot request it; the loop behaves as if that tool doesn't exist.
Yes, typically through tool_config with path or glob restrictions, so the agent can only read or write within a defined subset of the project.
web_search queries a search index and returns ranked results for the model to reason over. web_fetch retrieves the content of one specific, known URL. Enable the one that actually matches the task.
No meaningful runtime cost; scoping is evaluated before the loop even offers the tool to the model, so it doesn't add per-call latency.
For anything touching production systems, real user data, or irreversible actions, yes. Scoping limits what's possible; permission mode adds a check before the possible actually happens.
Yes, each subagent has its own allowed_tools and tool_config, independent of the parent's scope.
Start from the actual commands the task requires (inspect, test, build), test them manually first, and add to the list only as real needs surface rather than guessing broadly upfront.
It's not strictly required, but omitting it means file_edit and bash default to the process's ambient working directory, which is fragile across environments. Set it explicitly.
Tool scoping is set per query() call via AgentOptions; to change it mid-task, you'd typically end the current call and start a new one with updated options, optionally resuming the session.
Stack versions: Written against the Claude model lineup current as of ~June 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (the default), and Claude Haiku 4.5 - and the Claude Agent SDK (latest release, Python and TypeScript). Model names, SDK versions, and pricing move quickly - verify current specifics at platform.claude.com/docs before relying on them.
Reviewed by Chris St. John·Last updated Jul 16, 2026