Scoped tools in a research system
- Search agent
web_search,load_document - Analysis agent
Read,Grep, extraction tools - Synthesis agentno search; one scoped
verify_fact - Report agent
Writeto the reports folder only
Why fewer tools per agent
Every tool definition an agent can see is another option it must rule out on every turn. Anthropic’s docs say Claude’s ability to pick the right tool degrades once it has more than 30–50 available, and the engineering team notes that too many or overlapping tools distract agents from efficient strategies. Long before that limit, a second effect appears: an agent handed tools outside its specialism tends to use them. A synthesis agent that can search the web will start searching instead of synthesising what it was given, and the coordinator loses control of scope and cost.
The fix is the principle of least privilege applied to attention as well as safety: give each agent the tools its role needs and nothing more. That also gives each subagent a clear identity, which keeps the coordinator’s delegation decisions simple.
Scoping tools in the Agent SDK
In the Agent SDK a subagent is an AgentDefinition, and its tools field is the whitelist. Omit it and the subagent inherits every tool available to subagents. List tools and it gets only those — a tool you leave out is not in its session at all, so there is no permission prompt or error to deal with. disallowedTools works the other way, removing named tools; it also accepts server patterns such as mcp__crm__* to drop every tool from one MCP server.
options = ClaudeAgentOptions(
allowed_tools=["Agent"], # coordinator only delegates
mcp_servers={"research": research_server},
agents={
"searcher": AgentDefinition(
description="Finds and loads sources for a research question.",
prompt="Search, then load the most relevant documents.",
tools=["mcp__research__web_search", "mcp__research__load_document"],
),
"synthesiser": AgentDefinition(
description="Writes a cited summary from findings it is given.",
prompt="Use only the findings in your prompt. Verify single facts only.",
tools=["mcp__research__verify_fact"], # no search at all
),
"reviewer": AgentDefinition(
description="Checks a draft report against its sources.",
prompt="Report problems; never edit files.",
disallowedTools=["Write", "Edit", "Bash"],
),
},
)Two different lists are easy to confuse. The SDK separates availability — whether a tool is in Claude’s context at all — from permission — whether a call runs without a prompt. tools on an agent definition and bare names in disallowedTools change availability. allowedTools on the query only pre-approves calls; unlisted tools are still available and go through the permission flow. To stop an agent using a tool, remove it; do not merely leave it unapproved.
| Role | Typical tool set | Deliberately missing |
|---|---|---|
| Read-only analysis | Read, Grep, Glob | Edit, Write, Bash |
| Test execution | Bash, Read, Grep | Edit, Write |
| Code modification | Read, Edit, Write, Grep, Glob | Bash |
| Synthesis in a research system | One scoped fact-check tool | General web search |
Exceptions, and constrained tools
Strict separation has a cost. If the synthesis agent needs to confirm a date or a figure several times per report, sending every check back through the coordinator adds round trips. The usual answer is a limited cross-role tool: a narrow verify_fact that checks one claim against the sources already gathered, rather than full search. Anything that needs real research still goes back to the coordinator, which can dispatch the search agent.
The same thinking applies to generic tools. A fetch_url tool lets an agent fetch anything; a load_document tool that accepts only URLs from approved document stores does the job the role needs and nothing else. Constraining the tool is more reliable than asking the agent, in its prompt, to be careful with it.
Replace a generic tool with a constrained one
Generic
fetch_url
"Fetches any URL and returns
the response body."
input: { url: string }Constrained to the role
load_document
"Loads a document from the
firm's document store or the
regulator's filings site.
Other domains are rejected
with a validation error.
Returns text and page count."
input: { document_url: string }Configuring tool choice
On the Messages API, tool_choice controls whether and how Claude must use tools on a given request. It has four types, verified against the current docs:
tool_choice | Behaviour | Use it when |
|---|---|---|
{"type": "auto"} | Claude decides whether to call a tool. Default when tools are provided | Normal agent turns |
{"type": "any"} | Claude must call one of the tools, but picks which | A tool call is required; the right tool depends on the input |
{"type": "tool", "name": "…"} | Claude must call this specific tool | A step must happen first, or one schema must be filled |
{"type": "none"} | Claude cannot call tools. Default when no tools are provided | A turn that must be text only |
Which tool_choice does this step need?
- Model decides freely
autothe default - Some tool, never plain text
anyClaude picks which tool - This exact tool first
tool+ namethenautonext turn - No tool calls at all
nonetext-only turn
Three details matter. With any or tool, the API prefills the assistant turn to force a tool call, so Claude emits no explanatory text before the tool_use block — if you need a preamble, use auto and ask for the tool in the user message. Setting disable_parallel_tool_use: true inside tool_choice limits Claude to at most one call with auto, and exactly one with any or tool. And changing tool_choice between requests invalidates cached message blocks, though cached tool definitions and system prompts survive.
Force the first step, then let the model work
tool = extract_metadatatool_use: extract_metadatatool_choice autotool_choice is set per request. Force what must happen first, then return to auto for the rest of the task.Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Giving every agent every tool “for flexibility” | Scope each agent’s tools to its role; add narrow exceptions where needed. |
| Relying on a prompt to stop an agent using a tool it holds | Remove the tool from that agent with tools or disallowedTools. |
Confusing allowedTools with availability | allowedTools pre-approves calls; restrict availability with tools or bare disallowedTools. |
| Forcing one tool when the right one depends on the input | Use any so a call is guaranteed and Claude still chooses. |
Leaving tool_choice forced for the whole conversation | Force only the step that must come first, then return to auto. |
You should now be able to
- Assign each agent the minimum tool set for its role, with narrow cross-role exceptions.
- Restrict subagent tools with
AgentDefinition.toolsanddisallowedTools, and tell availability from permission. - Replace generic tools with constrained alternatives that enforce the role’s boundary.
- Choose between
auto,any, forcedtoolandnonefor a given step. - Force a first step with
tool_choice, then continue in later turns withauto.