Select And Apply Built-In Tools


11 Aug 2026  Istvan Dobrentei  8 mins read.

Part 12 · Domain 2, Task 2.5 - Select and apply built-in tools (Read, Write, Edit, Bash, Grep, Glob) effectively

Tools nobody has to define

Every tool in this series so far has been something the example project defined itself — a schema, a description, an implementation. Task 2.5 is about a different set: Read, Write, Edit, Bash, Grep, and Glob, the tools Claude Code ships with, that every session already has without anyone writing a single line of configuration for them.

They aren’t API primitives, so there’s nothing to call directly the way the rest of this series has called client.messages.create(). What’s testable is the same question every other part of Domain 2 has asked: given a stand-in with the same job and the same failure shape as the real tool, does the model pick it correctly, and does it recover the way the real tool expects it to?

Grep for content, Glob for paths

The two tools get confused because they’re both “find something” — but they answer different questions. Grep looks inside files; Glob looks at file names and paths, never at what’s inside them. Give an agent both, with descriptions that state exactly that distinction, and ask a question that only makes sense for one of them:

--- A content question: "Find every place that imports the Logger class." ---
run 1: Grep({'pattern': 'import.*Logger'})
run 2: Grep({'pattern': 'import.*Logger'})
run 3: Grep({'pattern': 'import.*Logger'})

--- A path-pattern question: "Find all test files matching **/*.test.tsx." ---
run 1: Glob({'pattern': '**/*.test.tsx'})
run 2: Glob({'pattern': '**/*.test.tsx'})
run 3: Glob({'pattern': '**/*.test.tsx'})

Clean three-for-three on both sides. There’s no real ambiguity here once the descriptions actually say what each tool looks at — this is the payoff of Task 2.1’s lesson landing on a pair of tools that are genuinely different jobs, not two versions of the same one. The harder question is what happens once a file is actually found and needs to change.

When Edit can’t find a safe anchor

Edit works by matching an exact string and replacing it — which only works if that string identifies exactly one place in the file. Most of the time that’s easy. It stops being easy when a file contains a real duplicate: the same code, copied and pasted, byte for byte identical.

FILES["orders.py"] = (
    "def process_order(order):\n"
    "    timeout = 30\n"
    "    return submit(order, timeout)\n"
    "\n"
    "def process_order(order):\n"
    "    timeout = 30\n"
    "    return submit(order, timeout)\n"
)

Two functions, same name, same body — an actual copy-paste bug, not a contrived example. Asked to fix the timeout in the second one only, here’s what happened, live:

turn 0: read_file({'filename': 'orders.py'}) -> is_error=False
turn 1: edit_file({'filename': 'orders.py', 'old_string': 'def process_order(order):\n    timeout = 30\n    return submit(order, timeout)\n\ndef process_order(order):\n    timeout = 30\n    return submit(order, timeout)', ...}) -> is_error=True
turn 2: write_file({'filename': 'orders.py', 'content': 'def process_order(order):\n    timeout = 30\n    return submit(order, timeout)\n\ndef process_order(order):\n    timeout = 60\n    return submit(order, timeout)\n'}) -> is_error=False

Final answer: Done. The first process_order definition keeps timeout = 30,
and the second now uses timeout = 60. Everything else remains unchanged.

It read the file first, on its own. It tried Edit with the largest anchor it could construct — the entire duplicated block — and Edit still refused, because no anchor of any size can uniquely identify “the second one” when both are literally identical text. That’s not a bug in the anchor; there genuinely isn’t a safe one. The moment Edit reported that, the model switched to reading the full file and writing it back with the one correct change in place — exactly the fallback the objective describes, and it only became necessary because the duplicate really was unresolvable any other way.

This is worth being precise about: Edit failing here isn’t a defect. A tool that requires a unique match and refuses an ambiguous one is doing its job correctly by refusing. The skill isn’t preventing the failure — it’s recognizing that this specific failure means “this isn’t Edit’s job for this change” and reaching for Read and Write instead, which is exactly what happened.

Reading with a plan instead of reading everything

The last piece isn’t about one tool call — it’s about how many turns it takes to actually understand something. Give an agent a codebase and a question, but only list_files and read_file — no way to search content, only open things and look:

--- Read-only tool set ---
turn 0: list_files({})
turn 1: read_file('pricing.py')
turn 1: read_file('api.py')
turn 1: read_file('auth.py')
turn 1: read_file('models.py')
turn 1: read_file('utils.py')
turn 1: read_file('tests.py')

Six files opened to answer a question that only concerned two of them. Nothing was wrong with the answer — it was accurate — but getting there meant reading a login function and a currency formatter that had nothing to do with the question. Add Grep back to the same tool set, same question:

--- Grep available ---
turn 0: grep({'pattern': 'calculate_discount'})
turn 1: read_file('pricing.py')
turn 1: read_file('api.py')

Three calls instead of seven. Grep found exactly which two files actually mentioned the function; Read only opened those. This is what “building understanding incrementally” actually looks like in practice — not a principle to follow on faith, but a direct, countable difference in how much of a codebase an agent has to pull into context before it can answer a question about a small part of it. Every file that gets read whether or not it turns out to matter is context spent for nothing, on top of the turns it costs to get there.

You can find the full file on GitHub. A PHP port is available too, built the same way as the earlier parts.

Critical design decisions

  • Grep and Glob split on content vs. path, not on “search” in general: a tool description that states this precisely — looks inside files, or looks at names and paths, never both — gives the model a real boundary to select on, the same lesson Task 2.1 covered for any other tool pair.
  • A unique-match requirement on Edit is a feature, not friction: refusing an ambiguous anchor is exactly what should happen when one genuinely doesn’t exist. The correct response to that refusal is switching tools, not fighting Edit into accepting a bigger anchor.
  • Read + Write is where Edit’s job actually ends: once a change can’t be safely scoped to a small, unique anchor, the honest move is reading the whole file and writing back the whole corrected version — not finding a technically-unique but oversized anchor that happens to work this one time.
  • Grep before Read turns “open everything” into “open what matters”: content search first is what makes it possible to only read the files a question actually concerns, instead of paying the context cost of every file in the codebase.
  • Entry points come from search, not from a full read of the project: the incremental pattern — Grep to find where something lives, Read to actually look at it — scales to a real codebase in a way that “read every file first” never will.

Consequences

  • Grep and Glob described as interchangeable “search” tools -> the model has no real basis to pick between them, and a path-pattern question can end up trying to search file contents for something that was never in any file’s text.
  • An Edit tool with no unique-match requirement -> a copy-paste duplicate gets “fixed” in the wrong location, or in both locations at once, silently, because nothing forced the ambiguity to surface before the change happened.
  • Treating an Edit failure as something to work around instead of listen to -> constructing a larger and larger anchor to force a technical match, when the actual problem is that this change was never a targeted edit to begin with.
  • No content search available during exploration -> every file in the codebase gets opened and read, whether or not it has anything to do with the question, burning both turns and context on files that were never going to matter.
  • Reading a whole project upfront instead of searching for entry points first -> the same problem at a larger scale: a real codebase makes “just read everything” completely impractical, while Grep-first scales the same as it did for six files.

Task 2.1 through 2.4 were all about tools this series had to build and describe from nothing. This part is different: Read, Write, Edit, Bash, Grep, and Glob already exist, already work, and already fail in specific, well-defined ways. The job isn’t designing them — it’s recognizing which one a given moment actually calls for, and taking a failure like Edit’s refusal as the answer to that question instead of an obstacle to push past.