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.
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().
Summary
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.
Recipe
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:
- You know exactly which tool families a task needs and want to grant only those.
- You're building an agent that will run unattended and need
permission_modeas a safety net. - You want file and bash operations scoped to a specific project directory via
cwd. - You're auditing an existing agent's tool access before deploying it.
Working Example
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:
- Combining three tool families (
bash,file_edit,web_fetch) for a task that genuinely needs to read history, fetch external data, and write a file. - Narrowing bash further with
allowed_commandsinstead of granting unrestricted shell access. - Deliberately omitting
web_searchbecause the task only needs to fetch known URLs, not search the web. - Printing tool-call events separately from text so you can see the loop's actions as they happen.
Deep Dive
How It Works
allowed_toolsis 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_editcovers reading and writing files undercwd; some SDK versions let you further restrict it to specific paths or globs viatool_config.bashexecutes shell commands in the working directory; scoping it to anallowed_commandslist (or a denylist, depending on SDK version) limits it to specific command prefixes rather than an open shell.web_searchqueries a search index and returns results the model can reason over;web_fetchretrieves 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_modeoperates independently ofallowed_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 Families at a Glance
| 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 |
Python Notes
# 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/**"]},
},
)Parameters & Return Values
| 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.) |
Gotchas
- Assuming an unset tool_config means "unrestricted." An unscoped
bashentry inallowed_toolscan mean full shell access on some configurations. Fix: always pairbashwith an explicitallowed_commandsor equivalent restriction unless you truly need arbitrary shell access. - Confusing
web_searchwithweb_fetch. Enablingweb_searchwhen the task only needs to read one known URL gives the agent a much larger, less predictable capability than it needs. Fix: enableweb_fetchalone for known-URL tasks; reserveweb_searchfor open-ended research. - Forgetting
cwdin multi-project environments. Without an explicitcwd,file_editandbashdefault to the process's working directory, which can differ between local runs and deployed environments. Fix: always setcwdexplicitly rather than relying on the ambient directory. - Treating
permission_modeas a substitute for scoping. A broadallowed_toolslist 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. - Granting
file_editto a read-only reporting agent. If a task never needs to write files, includingfile_editanyway is unnecessary surface area. Fix: grant only the tools the task's actual outputs require. - Not testing scoped
bashcommands ahead of time. Anallowed_commandslist 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.
Alternatives
| 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 |
FAQs
Do I have to enable all four built-in tool families together?
No. Enable only what the task needs; allowed_tools accepts any subset, and an empty or minimal list is valid for pure-text tasks.
What happens if the model tries to call a tool that isn't in allowed_tools?
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.
Is bash access always dangerous?
- Unrestricted bash is high-risk since it can run any shell command.
- Scoped bash, limited to specific command prefixes, is much lower risk.
- Risk also depends on what permission_mode and checkpoints sit on top of it.
Can I restrict file_edit to specific files or folders?
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.
What's the difference between web_search and web_fetch?
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.
Does scoping tools slow down the agent?
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.
Should I always use permission_mode alongside scoping?
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.
Can tool scoping differ between a parent agent and its subagents?
Yes, each subagent has its own allowed_tools and tool_config, independent of the parent's scope.
How do I know which commands to put in an allowed_commands list?
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.
Is cwd required?
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.
Can I change tool scoping mid-session?
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.
Related
- The Claude Agent SDK Mental Model - how tool scoping fits into the broader loop
- Claude Agent SDK Basics - a first query() with default tools
- Adding Human-in-the-Loop Checkpoints to an Agent SDK Workflow - the approval layer on top of scoping
- Claude Agent SDK Configuration Options Reference - full option comparison across Python and TypeScript
- Connecting the Claude Agent SDK to MCP Servers - extending reach beyond built-in tools
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.