Build your own Claude Code
The Bash tool & your first real fix·12 min

Register the toolset

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage register-tools --watch

You've written six tool methods on Tools — view, create, insert, strReplace, glob, grep, bash — but the loop doesn't know they exist. The loop speaks one verb: it hands you a tool name (a string the model chose) and an input object, and expects a tool_result back. The thing that turns "Bash" into tools.bash(input.command) is the dispatcher.

It's a small function, but it's the seam where the model's vocabulary meets your implementation. Get the routing right and every tool the model can name resolves to real work; get it wrong and the agent silently calls the wrong method or crashes on an unknown name.

One wrinkle: each tool answers to two names. The model was trained on Anthropic's client-tool names (Read, Write, Edit, Glob, Grep, Bash), while the text-editor primitives underneath are view / create / str_replace. Your dispatcher routes both spellings to the same method, plus a trivial echo for smoke tests. Anything it doesn't recognize must come back as an error result, not a thrown exception — an unknown tool is an observation the model should see and recover from, not a crash that kills the loop.

What to build

Your task

Implement the execute dispatcher

Implement makeExecute(tools), returning an execute(name, input) function that routes each tool name to a Tools method and returns its ToolOutput. Wire every alias: Read/view → view, Write/create → create, Edit/str_replace → strReplace, insert → insert, Glob/glob → glob, Grep/grep → grep, Bash/bash → bash, and echo → { content: input.text }. Read each method's arguments off input (input.path, input.command, input.old_str/input.new_str, input.pattern, …). For any unknown name, return { content: 'unknown tool: <name>', is_error: true }.

src/program.ts
function makeExecute(tools: Tools): Execute {
  return (name, input: any) => {
    switch (name) {
      // route each tool name (and its alias) to the matching Tools method;
      // unknown name -> { content: `unknown tool: ${name}`, is_error: true }
    }
  }
}
Unknown tool → error result, never a throw

The default branch is load-bearing. If the model hallucinates a tool name, the dispatcher must return { content: 'unknown tool: …', is_error: true } so the loop feeds it back as a tool_result and the model can correct course. Throwing instead would unwind the loop and fail the run on a recoverable mistake.

Input keys are snake_case on the wire

The model emits Anthropic-shaped inputs: file_text, old_str, new_str, insert_line. Your Tools methods take camelCase arguments. The dispatcher is where you translate — e.g. tools.strReplace(input.path, input.old_str, input.new_str). Mis-mapping a key is the most common way this step fails.

How it's graded

program.ts dispatch seeds a sandbox, then calls execute(tool, input) for each entry in the case's calls array and prints { results }. The grader checks that array exactly: a Read of a seeded a.txt: "A" yields {"content":"1: A"}, a Bash echo hi yields {"content":"hi\n","is_error":false,"exit_code":0}, and a Glob ** yields {"content":"a.txt"} — every alias routed to the right method, in order.