Learn Claude Code
s02

Tools

Tools & Execution

One Handler Per Tool

237 LOC4 toolsTypeScriptTool dispatch map
The loop stays the same; new tools register into the dispatch map

s01 > [ s02 ] s03 > s04 > s05 > s06 | s07 > s08 > s09 > s10 > s11 > s12

"Adding a tool means adding one handler" -- the loop stays the same; new tools register into the dispatch map.

Harness layer: Tool dispatch -- expanding what the model can reach.

Problem

With only bash, the agent shells out for everything. cat truncates unpredictably, sed fails on special characters, and every bash call is an unconstrained security surface. Dedicated tools like read_file and write_file let you enforce path sandboxing at the tool level.

The key insight: adding tools does not require changing the loop.

Solution

+--------+      +-------+      +------------------+
|  User  | ---> |  LLM  | ---> | Tool Dispatch    |
| prompt |      |       |      | {                |
+--------+      +---+---+      |   bash: run_bash |
                    ^           |   read: run_read |
                    |           |   write: run_wr  |
                    +-----------+   edit: run_edit |
                    tool_result | }                |
                                +------------------+

The dispatch map is a dict: {tool_name: handler_function}.
One lookup replaces any if/elif chain.

How It Works

  1. Each tool gets a handler function. Path sandboxing prevents workspace escape.
function safePath(relativePath: string): string {
  const filePath = resolve(WORKDIR, relativePath);
  const normalizedWorkdir = `${WORKDIR}${process.platform === "win32" ? "\\" : "/"}`;
  if (filePath !== WORKDIR && !filePath.startsWith(normalizedWorkdir)) {
    throw new Error(`Path escapes workspace: ${relativePath}`);
  }
  return filePath;
}
  1. The dispatch map links tool names to handlers.
const TOOL_HANDLERS = {
  bash: (input) => runBash(String(input.command ?? "")),
  read_file: (input) => runRead(String(input.path ?? ""), Number(input.limit ?? 0) || undefined),
  write_file: (input) => runWrite(String(input.path ?? ""), String(input.content ?? "")),
  edit_file: (input) =>
    runEdit(String(input.path ?? ""), String(input.old_text ?? ""), String(input.new_text ?? "")),
};
  1. In the loop, look up the handler by name. The loop body itself is unchanged from s01.
for (const block of response.content) {
  if (block.type !== "tool_use") continue;

  const handler = TOOL_HANDLERS[block.name as ToolUseName];
  const output = handler
    ? handler(block.input as Record<string, unknown>)
    : `Unknown tool: ${block.name}`;

  results.push({
    type: "tool_result",
    tool_use_id: block.id,
    content: output,
  });
}

Add a tool = add a handler + add a schema entry. The loop never changes.

What Changed From s01

ComponentBefore (s01)After (s02)
Tools1 (bash only)4 (bash, read, write, edit)
DispatchHardcoded bash callTOOL_HANDLERS dict
Path safetyNonesafe_path() sandbox
Agent loopUnchangedUnchanged

Try It

cd learn-claude-code
cd agents-ts
npm install
npm run s02
  1. Read the file package.json
  2. Create a file called greet.ts with a greet(name: string) function
  3. Edit greet.ts to add a JSDoc comment
  4. Read greet.ts to verify the edit worked