Agent Mentor Learn
Agent Tool Calling: Getting Agents to Actually Do Things · Lesson 3 of 6

Lesson 3: Five Common Tool Types: Read, Write, Execute, Search, Call

Learning goals:

  • Sort common tools into five categories by how much damage they can do, and name the typical signature of each
  • Explain why command-execution tools sit in a different risk class from the other four
  • Explain why search tools return matching snippets instead of whole files

Prerequisites: finished Lesson 2, you understand the round-trip shape of a tool call | Prev: Lesson 2 << | Next: Lesson 4 >>

Start with a table

ToolTypical inputWhat it returnsWorst case when it goes wrong
Read filepathFile contents (string)Reads a file it shouldn't, leaks information
Write filepath, contentSuccess/failure statusOverwrites work someone hasn't saved yet
Execute commandcommandstdout/stderr/exit codeWipes a database, sends requests, installs a poisoned package — irreversible
Searchquery, pathList of match locations + snippetsReturns so much it blows out the context, or misses the key result
Call external APIStructured params (varies by service)JSON/error objectSpends someone's money, sends the wrong message, gets stale data

What sorts this table? Not the alphabet. It's "how wide a range of harm one call can cause" — the blast radius of that call. A read-only tool has a blast radius of roughly zero: reading the wrong file just derails this one turn of the conversation. Writing a file can overwrite existing content. Executing a command can do anything at all to the whole system. As we walk through each category, you'll see that beyond "what it can do," each one carries a pitfall that only that category trips over.

Read: safest, but not zero risk

A read-file tool usually has a signature like this:

The return value is the file contents themselves, usually with line numbers so the model can reference them later:

1  export function add(a, b) {2    return a + b;3  }

Reading a file changes no state. If the model reads the wrong thing, or reads too much, the worst outcome is some irrelevant content in this one turn — and the model tends to notice it read the wrong thing and read again. That's why it's called the "safest" category: not that it carries no risk, but that the risk can't escape the bounds of this conversation.

The real risk is reading a file it should never have touched. If the agent has permission to read ~/.ssh/id_rsa or the project's .env, an innocent-looking "show me what's in this directory" can lift a secret key verbatim into the conversation context. From there, the leak has already happened the moment that context gets emitted by the model, written to a log, or carried out by some later "call external API" tool. That's why read-file tools almost always go together with a path allowlist or a sandbox, rather than "it's read-only, just hand it the keys." Lesson 5 covers how to set that kind of boundary in detail.

Write: where the consequences stop being symmetric

A write-file tool has one more parameter than read, and one less bit of safety:

The return value is usually simple, just a status:

The problem isn't the return value, it's the call itself. If a read goes wrong, you read again and nothing has changed. If a write goes wrong — say the model fills in the wrong path, or the content is missing half of what it should be — the original file's contents are already overwritten and can't be recovered, unless there's version control or a backup. This is the "asymmetry between read tools and write tools": the two call shapes look almost identical (a path plus a couple of parameters), but one can be retried freely and the other gambles on every single call.

So a responsible write tool adds a layer of protection — for instance, requiring that the file was read before it can be edited (to stop the model editing from memory), or returning a diff of old versus new content instead of a bare "success," so the caller (the host application) has a chance to show the change before it actually hits disk. Those aren't the focus here; Lesson 4 opens them up when it covers interface design.

Execute: a risk class of its own

The command-execution tool has the plainest-looking signature of the five:

One string goes in; stdout, stderr, and an exit code come out:

The catch is that this command field is essentially an open-ended entry point — it isn't a specific, schema-bounded operation like "delete this file" or "read this line," it's an arbitrary shell script. rm -rf, a curl that ships data to an outside server, an npm install that pulls in a poisoned package — all of it fits inside that one string. The other four categories (read, write, search, call API), however you design their signatures, are limited by their parameter structure in what they can do. A command-execution tool's capability boundary is the whole operating system's capability boundary. That's why it's in a class of its own: not "a bit riskier," but a different order of magnitude of risk.

For exactly that reason, the official docs design operating-system-level isolation specifically for this category: filesystem access and network access are two separate sandbox layers, and even if the model is steered by a prompt injection and insists on running a dangerous command, the OS boundary holds regardless — it doesn't depend on whether the model "wants" to cooperate1. The stated motivation is blunt: the goal is that even a successful prompt injection is fully contained and can't escape the sandbox2. Lesson 5 covers how to configure that isolation; for now, hold on to one thing: wherever the "execute command" signature shows up, treat it by default as the category in the five that most needs extra constraint.

Search: returns locations, not the whole world

A search tool (say, one that finds a keyword or regex across a codebase) often carries a "limit how much comes back" parameter in its signature:

The return value isn't the files themselves, it's "where the match is and what the surrounding context looks like":

If this tool just stuffed the full contents of every matched file back in, two problems show up. The first is a token problem: one search hits 50 files, each a few hundred lines, all of it poured into the context — and this single tool call has eaten the whole turn's input budget, leaving nothing for the model to keep working with3. Writing tool descriptions and controlling the bounds of input and output is itself a basic requirement for making a tool usable4. The second problem matters more: the point of search isn't "read through everything that might be relevant," it's "help the model figure out where to look next." Return the match locations plus a short snippet of context, the model reads those snippets and judges for itself — "of these results, the second one looks like what I'm after, let me read that file's full contents on its own." That's how a search tool and a read tool work together: search narrows the range, read gets the detail. Returning "match locations" rather than "whole files" is exactly the follow-up lead the model needs, instead of a one-time dump of everything that might be useful.

Call external API: failure is the norm, not the exception

The first four categories mostly stay inside the local system. Calling an external API is different — it crosses the network, to a service you don't control:

A normal return looks like this:

But an external service will rate-limit you, time out, reject a request for missing permissions, and change its own interface in the gap between your calls. These aren't "unexpected situations," they're the normal running conditions for this category. What actually decides whether the tool is any good isn't "what it returns when things are fine," it's "what it returns when things fail":

This error message isn't for you, it's for the model — whether it should retry or switch strategy next depends on whether it can read that error field. The MCP specification writes this straight into the protocol: clients should provide tool execution errors to language models so the model has a chance to self-correct and try again5. In other words, a tool that silently swallows a 429 and returns nothing but "call failed" is robbing the model of the chance to correct itself; a tool that carries back concrete detail like retry_after is the one designing "failure" as a normal part of the workflow.

The call-external-API category also drags in another layer of risk: if this agent can read private data at the same time as it's exposed to untrusted content (a chunk of web text a user pasted, say) and it can also send messages or requests outward, those three together are what security research calls the "lethal trifecta" — the attacker doesn't need to break into your system, they just hide an instruction in content the agent will read and let the agent carry the private data out itself6. Lesson 5 opens this topic up on its own; for now, know this: calling an external API is the last and most critical link in that chain, because it's the exit through which data actually leaves your system.

Recap

  • The risk across the five categories isn't evenly spread: read file has the lightest consequences, write file is where things start being irreversible, execute command's blast radius equals the whole operating system, and search and call-external-API each have their own separate pitfalls
  • The core difference between read tools and write tools is whether you can safely retry — read wrong and you just read again, write wrong and the original content may be gone for good
  • Command-execution tools need an OS-level sandbox to fall back on because their command parameter is an open string, not bounded by a schema structure the way the other four are1 2
  • Search tools return match locations plus snippets rather than whole files, first to save tokens, and second to split "locate" from "read the detail," handing the model a lead it can follow up on3
  • Call-external-API tools should carry failure information (error type, whether it can be retried) back to the model as-is rather than swallowing it — in this category, failure is the norm, not the exception5

Next lesson, we take the "signatures" of these five categories apart: how to write a good tool name, description, parameter schema, and return value, so the model calls it right the first time.

>> Lesson 4: Designing Tool Interfaces: Name, Description, Parameters, Return Value

Footnotes

  1. Configure the sandboxed Bash tool - Claude Code Docs — https://code.claude.com/docs/en/sandboxing 2

  2. Making Claude Code more secure and autonomous with sandboxing - Anthropic Engineering — https://www.anthropic.com/engineering/claude-code-sandboxing 2

  3. Introducing advanced tool use on the Claude Developer Platform | Anthropic Engineering — https://www.anthropic.com/engineering/advanced-tool-use 2

  4. Writing effective tools for AI agents—using AI agents | Anthropic Engineering — https://www.anthropic.com/engineering/writing-tools-for-agents

  5. Tools - Model Context Protocol — https://modelcontextprotocol.io/docs/concepts/tools 2

  6. The lethal trifecta for AI agents - Simon Willison's Weblog — https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/

Exercises

01

Below are three draft results from tool calls, each with a problem. Say which category of tool the problem belongs to (read/write/execute/search/call API), explain why the design is a poor fit, and give the change you'd make.

Level 1: Pick the return value for a tool
  1. search_code returns: { "content": "<the full source of 50 files stitched together, 8000 lines in total>" }
  2. write_file returns: { "success": true } (no diff, no old-content information)
  3. send_email on failure returns: { "error": "failed" }
Done criteria · checked locally
02

You need to wire up a tool for an agent: it periodically checks the shipment status of a third-party logistics API, and if the status turns to "abnormal," it writes the tracking number and the reason into a local alerts.log file.

Level 2: Pick the tool types for a new scenario and design a signature

This task actually involves more than one category of tool. Write out:

  1. Which of this lesson's five categories does it need? What is each responsible for?
  2. Write a JSON input_schema for the "call external API to query shipment status" tool, with at least a tracking-number parameter (the systematic take on interface design is next lesson; here just mimic the signature format that's shown up in this lesson)
  3. When this tool's query fails (tracking number doesn't exist, request times out), what should the return value look like?
Done criteria · checked locally