Study guides / CCAR-F / Domain 2

Tool Design & MCP Integration · Lesson 5 of 5

2.5 - Built-in Tools

Selecting and applying built-in tools (Read, Write, Edit, Bash, Grep, Glob) effectively for codebase operations

Claude Code provides six built-in tools for working with codebases: Read, Write, Edit, Bash, Grep, and Glob. Each has a specific purpose, and using the wrong tool for a task wastes time, context tokens, or both. The exam deliberately presents scenarios where confusing these tools leads to incorrect answers.

Grep vs Glob: The Core Distinction

This is the distinction that matters most in this task statement. Get it wrong and you'll lose marks.

Grep searches file CONTENTS for patterns. Use Grep when you need to find text inside files. Function callers. Error messages. Import statements. Variable assignments. Any time you are searching for what files contain, Grep is the tool.

// Find all files that call processLegacyOrder()
Grep: "processLegacyOrder"

// Find all error messages containing "timeout"
Grep: "timeout"

// Find all files that import a specific module
Grep: "import.*from 'utils/auth'"

Glob matches file PATHS by naming patterns. Use Glob when you need to find files by name, extension, or directory structure. Test files. Configuration files. All TypeScript files in a specific directory. Any time you are searching for files based on their path, Glob is the tool.

// Find all test files
Glob: "**/*.test.tsx"

// Find all configuration files
Glob: "**/config.*"

// Find all MDX files in the domains directory
Glob: "content/domains/**/*.mdx"

The distinction in one sentence: Grep finds what is INSIDE files. Glob finds files by their NAMES.

The exam presents scenarios where a developer uses the wrong tool. Use Glob to find function callers and it fails - Glob matches paths, not contents. Use Grep to find test files by naming pattern and it works technically (by searching for "test" in filenames via content), but it's the wrong tool, and the exam expects you to identify the correct one.

Read, Write, and Edit

These three tools handle file operations, each optimised for a different use case.

Edit performs targeted modifications using unique text matching. You specify the exact text to find and its replacement. It's fast and precise because it touches only the specific text you identify.

Edit:
  old_string: "function processOrder(id: string)"
  new_string: "function processOrder(id: string, validate: boolean = true)"

When Edit fails: Edit requires unique text matching. If the text you specify appears in multiple places in the file, Edit can't tell which occurrence you mean, so it fails. That's a safety mechanism, not a bug - it stops you changing text you never meant to touch.

When Edit can't find a unique anchor. The fix per the Edit tool docs is to widen old_string with more surrounding context until it pins down one location, or set replace_all: true if you actually want every occurrence updated. Both options keep you on Edit and cost almost no extra context. Read + Write (load the whole file, write the whole file back) is the last resort. It works. But you've now spent a file's worth of tokens on what was usually a one-line change.

The ordering:

  1. Try Edit with the shortest anchor that's plausibly unique.
  2. On a non-unique match, widen old_string until it matches one location, or use replace_all: true if you want every occurrence changed.
  3. Only fall back to Read + Write when neither of those can disambiguate the target.

Don't default to Read + Write for every modification. The exam penalises that because it burns context tokens. It also penalises jumping straight from a non-unique Edit failure to Read + Write. Widening the anchor or using replace_all is the documented response. Read + Write is the fallback, not the next step.

Incremental Codebase Understanding

How you explore a codebase matters as much as which tools you use. There's a right way and a wrong way.

Wrong: Read all files upfront. Loading every file into context before you know what you need is a context-budget killer. A 200-file codebase read in full swallows your entire context window, mostly on files that have nothing to do with your task. No other exploration mistake costs you more.

Right: Incremental discovery. Start narrow. Expand only as needed.

  1. Grep to find entry points. Search for the function name, class name, or error message that anchors your investigation. This tells you which files are relevant.

  2. Read to follow imports and trace flows. Once you know which files matter, Read them to understand the code structure. Follow import statements to discover related files.

  3. Grep again to trace usage. If you find a wrapper function or re-export, Grep for that name across the codebase to find all consumers.

  4. Read only what you need. Each file you read should be justified by what you discovered in the previous step.

That's minimal context for maximum understanding. You map the codebase progressively, spending tokens only on files that matter to the task.

Tracing Function Usage Across Wrapper Modules

A common codebase pattern: a function is defined in one module, re-exported through a wrapper, and consumed through the wrapper's name. A simple Grep for the original name misses every consumer who imports through the wrapper.

The correct approach:

  1. Grep for the function definition to find where it is defined
  2. Read the defining file to identify exported names
  3. Grep for each exported name across the codebase to find all consumers
  4. If the function is re-exported through a barrel file (e.g. index.ts), Grep for the barrel file's module name to find consumers who import from it

The multi-step trace catches indirect consumers a single Grep would miss.

The Deprecation Scenario

This one turns up constantly in exam prep: find every file that calls a deprecated function AND the test files that exercise it. The correct sequence:

  1. Grep for the function name - finds every file whose contents reference the function, including any tests that import it directly (content search)
  2. Glob for sibling test files - finds the test file that pairs with each caller by naming convention, e.g. OrderProcessor.tsOrderProcessor.test.tsx, even when the test exercises the function indirectly through the source module (path matching)
  3. Grep again for wrapper names - when a caller exposes the function through a wrapper (e.g. applyLegacyOrder calls processLegacyOrder internally), Grep for the wrapper name to find tests that cover the function transitively through it

Say Grep reveals that OrderProcessor.ts and RefundHandler.ts call the deprecated function. Glob for **/OrderProcessor.test.* and **/RefundHandler.test.* to pull in their sibling test files, even if those tests never mention processLegacyOrder by name. And if either source file wraps the function under a new name, Grep for the wrapper to catch any remaining tests.

This is Grep, then Glob, then Grep again - content search for direct references, path matching for adjacent tests, content search for indirect coverage. Not Glob first.

Key Concept

Grep searches file contents. Glob matches file paths. Edit is the default for modifications. On a non-unique match, widen the anchor or use replace_all: true. Read + Write is the last-resort fallback. Build codebase understanding incrementally. Never read all files upfront.

Exam traps

Practice question

A developer needs to find all files that call a deprecated function processLegacyOrder() and also find all test files for those callers. Which tool sequence is correct?

  • A Glob for **/*processLegacyOrder* to find caller files, then Grep inside that result set for test files. Glob resolves the file list first, so the content search runs over fewer files and stays inside the context budget.

    Glob matches file paths, not file contents. It cannot find function callers - it would only match files named after the function, which is unlikely. The tools are backwards.

  • B Read all the source files to search for the function manually, then Read all the test files to pair them with their callers. Reading every file gives complete visibility of each call site and test, so no caller can be missed by a naming mismatch, and the full contents remain available in context for the later refactoring steps.

    Reading all files upfront is a context-budget killer. It consumes tokens on irrelevant files and is the exact anti-pattern the exam penalises.

  • C Grep for processLegacyOrder to find callers (this also surfaces tests that import the function directly), then Glob for the sibling test file of each caller (e.g. **/OrderProcessor.test.*) to catch tests that exercise the function through the source module without naming it. Correct

    Grep searches file contents - correct for finding callers and any tests that reference the function by name. Glob matches file paths - correct for finding the test file paired with each source file by naming convention, which is how tests exercise the function indirectly. This is the optimal sequence.

  • D Bash with find and xargs grep for both steps, since a single shell pipeline can locate the callers and their test files in one pass without switching between built-in tools.

    While technically functional, this bypasses the built-in tools designed for these tasks. The exam expects candidates to select the right built-in tool for each task.

Build exercise: Trace and Refactor a Deprecated Function Using Built-in Tools

Intermediate · 30 minutes

You'll practice:

  1. Use Grep to search for all callers of a target function (e.g. processLegacyOrder) across the codebase

    Grep searches file contents - it is the correct tool for finding function callers. Using Glob here would fail because Glob matches file paths, not contents. The exam tests this distinction directly and penalises candidates who confuse the two.

    You should see: A list of file paths containing calls to processLegacyOrder, with line numbers and matching lines showing the exact call sites. For example: src/OrderProcessor.ts:42: await processLegacyOrder(orderId).

    Hints
    1. Grep searches what is inside files. You want to find which files contain calls to the function, so Grep is the correct tool.
    2. Run Grep with the function name as the pattern. No need for regex - a literal string match finds all call sites, imports, and references.
    3. Use the Grep tool with pattern "processLegacyOrder" and no path restriction to search the entire codebase. The results show every file and line that references this function.
  2. Use Glob to find test files matching the caller filenames (e.g. **/*.test.tsx)

    Glob matches file paths by naming pattern - it is the correct tool for finding test files by extension or naming convention. This completes the Grep-then-Glob pattern: content search to find callers, then path matching to find their tests.

    You should see: A list of test file paths matching the pattern, such as src/OrderProcessor.test.tsx and src/RefundHandler.test.tsx. These correspond to the caller files found by Grep in the previous step.

    Hints
    1. You need to find files by their name pattern (ending in .test.tsx), not by their contents. That means Glob, not Grep.
    2. Use a Glob pattern like **/*.test.tsx to find all test files, or narrow it to specific filenames like **/OrderProcessor.test.* if you know the caller names from the Grep results.
    3. Use the Glob tool with pattern "**/*.test.tsx" to find all test files. For targeted results, use patterns like "**/OrderProcessor.test.*" for each caller file identified in step 1.
  3. Use Read to examine each caller file and understand the usage pattern and context

    Reading files incrementally - only after Grep identifies which files matter - is the correct approach. Reading all source files upfront is a context-budget killer that the exam explicitly penalises. Each Read should be justified by what you discovered in the previous step.

    You should see: The full contents of each caller file, showing how processLegacyOrder is called, what parameters are passed, how the return value is used, and whether the function is imported directly or through a wrapper module.

    Hints
    1. Only Read files that Grep identified as callers. Do not Read files speculatively - each Read should be justified by the Grep results.
    2. Look for import statements at the top of each file. If the function is imported through a barrel file (e.g. import { processLegacyOrder } from "./utils"), you may need to trace the re-export chain.
    3. Use the Read tool on each file path from the Grep results. Check the import statement to see if the function is imported directly (from "./legacyOrders") or through a barrel file (from "./utils"). If it is re-exported, Grep for the barrel file module name to find indirect consumers.
  4. Use Edit to replace the deprecated function call with the new API in each caller file

    Edit is the preferred modification tool because it targets specific text and uses less context than Read + Write. The exam penalises defaulting to Read + Write for every modification. Always try Edit first - it is faster and more precise.

    You should see: Each caller file updated with the new API call replacing the deprecated one. For example, processLegacyOrder(orderId) replaced with processOrder(orderId, { validate: true }). The Edit tool confirms the replacement was made successfully.

    Hints
    1. Edit requires the old_string to be unique in the file. Provide enough surrounding context in old_string to make it unique if the function name alone appears multiple times.
    2. Specify both old_string (the exact text to find) and new_string (the replacement). Include the full function call with its arguments to ensure a unique match.
    3. Use the Edit tool with:
        old_string: "await processLegacyOrder(orderId)"
        new_string: "await processOrder(orderId, { validate: true })"
      If the function call appears with different arguments in different places, include surrounding code to make old_string unique.
  5. When Edit fails with a non-unique match, widen old_string with more surrounding lines until it pins down one location (or set replace_all: true if you actually want every occurrence updated). Only fall back to Read + Write if neither option can disambiguate the target

    Edit fails when the target text appears multiple times in the file - this is a safety mechanism, not a bug. Per the Edit tool documentation, the documented recovery is to expand the anchor with more surrounding context until it matches one place, or to use replace_all for global replacements. Both keep you on Edit and cost almost nothing in context. Read + Write loads the entire file for what is usually a single-line change - keep it as a last resort.

    You should see: On the first try, Edit fails with an error like: old_string matches 3 locations. On the retry with a wider old_string that includes the surrounding function name or unique adjacent line, Edit succeeds and changes exactly one occurrence. If replace_all: true was the right call, every occurrence is updated atomically.

    Hints
    1. Read the surrounding lines (one Read with the affected line range is fine) to find a unique anchor - usually the enclosing function name, a distinctive nearby comment, or specific argument values.
    2. If you genuinely want every occurrence changed, use replace_all: true on the original short anchor instead of multiple Edits.
    3. // Attempt 1 - fails
      // Edit { old_string: "processLegacyOrder(orderId)", new_string: "processOrder(orderId, { validate: true })" }
      // Error: old_string matches 3 locations
      
      // Attempt 2 - widen the anchor with surrounding context
      // Edit {
      //   old_string: "async function handleRefund(orderId: string) {\n  const result = await processLegacyOrder(orderId)",
      //   new_string: "async function handleRefund(orderId: string) {\n  const result = await processOrder(orderId, { validate: true })"
      // }
      // Success - only the handleRefund occurrence changes.
      
      // Or, if you want every occurrence updated atomically:
      // Edit { old_string: "processLegacyOrder(orderId)", new_string: "processOrder(orderId, { validate: true })", replace_all: true }

Sources