Which built-in tool answers this question?
- Files matching a name patternGlobe.g.
**/*.test.tsx - Where some text appearsGrepfunction names, errors, imports
- What one file saysReadthen follow what it imports
- Change or run somethingEdit · Write · Bashtargeted edit, new file, command
The six tools and their jobs
| Tool | Does | Reach for it when | Permission prompt? |
|---|---|---|---|
Glob | Finds files by path pattern, newest first, up to 100 results | You know the shape of the file name: src/**/*.ts, *.{json,yaml} | No |
Grep | Searches file contents with ripgrep regex; skips gitignored files | You know text that should be inside: a function name, an error string | No |
Read | Returns a file with line numbers; also images, PDFs, notebooks | You have a specific file to understand | No, inside the project |
Edit | Replaces an exact, unique string in a file | A targeted change to an existing file | Yes |
Write | Creates a file or overwrites it entirely | A new file, or a rewrite of a small one | Yes |
Bash | Runs a shell command | Tests, builds, git, anything no dedicated tool covers | Yes |
Two details decide many answers. Grep’s default output mode is files_with_matches — just paths — which is often all you need to decide what to read next; content adds matching lines with line numbers, and count gives totals. Its glob and type filters narrow a search to, say, **/*.tsx or Python files. Glob, by contrast, never looks inside a file: searching for a function name with Glob can only match file names.
Explore incrementally: search, then read
The costly mistake with an unfamiliar codebase is reading lots of files “to get context”. Every file read lands in the context window, most of it irrelevant, and the details that matter get diluted. Anthropic’s context-engineering guidance describes the better pattern, and names Claude Code as an example: keep lightweight references such as file paths, and pull content in just in time with tools like glob and grep. Each step tells you where to look next.
Tracing a feature through unfamiliar code
- Grep for an entry pointroute, command name or error text
- Read that filenote what it imports and calls
- Grep for each namefind definitions and every caller
- Read only what mattersthen make the change
repeat until the flow is understood
Changing files: Edit, and when to fall back
Edit is the precise tool: give it an old_string and a new_string, and it replaces that text. Three rules make it safe. The file must have been read in the conversation first — and a read cut short by a partial-view notice does not count (newer models can skip this in some cases). The old_string must match exactly: one character of whitespace or indentation is enough to miss. And it must appear exactly once, unless you set replace_all: true to change every occurrence.
Why did this Edit fail?
- Passes: File was read in this conversationa full read, not a partial view
- Passes:
old_stringmatches character for characterindentation included - Fails:
old_stringappears exactly onceappears 3 times in the file - Check: Every occurrence should changeonly then use
replace_all: true
replace_all only if every copy should change.When the text is not unique, first give Edit more context — the lines around the one occurrence you mean. If no stretch of text reliably pins it down, fall back to reading the whole file and writing the corrected version with Write. That trades precision for certainty, so keep it for small files or genuinely tangled cases: rewriting a large file risks silently dropping content you did not mean to touch.
Edit config.ts
old_string: timeout: 30
→ error: old_string appears 3 times
Edit config.ts # 1. add context to pin one occurrence
old_string: payments: {\n timeout: 30
new_string: payments: {\n timeout: 60
# 2. If no unique context exists: Read the whole file, then Write it back
# with the one change. Check the diff before moving on.Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Reading every file up front to “build context” | Grep for an entry point, Read it, and follow imports and names outward. |
| Using Glob to find where a function is called | Use Grep; Glob matches file paths, not contents. |
| Retrying the same Edit after a non-unique match error | Add surrounding context, use replace_all if every copy should change, or Read then Write. |
| Rewriting a whole large file with Write for a one-line change | Use Edit for targeted changes; keep Write for new files or small rewrites. |
| Tracing a function only through its wrapper module | List the wrapper’s exported names and Grep for each across the codebase. |
You should now be able to
- Choose Grep for content search and Glob for path patterns.
- Use Read for a specific file and Write for new files or full rewrites.
- Make targeted changes with Edit, and recover when
old_stringis not unique. - Explore an unfamiliar codebase incrementally: search, read, follow imports, search again.
- Trace a function across wrapper modules by searching for each exported name.