File and search tools
Read, write, edit by exact replacement, glob and grep inside one folder.
Create a free account Try part of it here
Why it matters
Coding agents spend most of their time reading, searching and editing files. The tools for that look simple, but their design decides whether the agent is precise or clumsy, and whether it's safe. Claude Code and similar harnesses settled on a small set with careful rules. You'll build the same set, and learn the reason behind each rule.
What you'll be able to do
- Design the five core file tools a coding agent needs: read, write, edit, glob and grep
- Keep every path inside one workspace folder, even with tricks like ../
- Edit files by exact string replacement, and explain why it beats line numbers and rewrites
- Refuse to overwrite a file the agent hasn't read, or one that changed since it was read
What it covers
- The five tools
- Edit by exact replacement
- Why the match must be unique
- Keep paths inside the workspace
- Paths that try to escape
- Read before you write
- Catching a stale write
- Output a model can use
- Checks that look right and aren't
The five tools
A carpenter could do everything with one big knife, and would do it badly. Coding agents get a small set of specific tools:
| tool | does | key design choices |
|---|---|---|
| read | returns lines of a file | line numbers in the output; offset and limit for big files |
| write | creates or replaces a whole file | for existing files, only after reading them |
| edit | replaces an exact string in a file | the old text must match exactly once |
| glob | finds files by name pattern (src/**/*.py) | sorted, capped results |
| grep | finds lines matching a regex | path:line: text output, capped |
A shell tool could do all of this with cat, sed and find. Dedicated tools are better because the harness can check paths, cap output, track what was read, and show the user a clean diff before approving a change.
Dedicated tools give safer, cheaper and easier-to-check results than a shell.
Edit by exact replacement
Say you're correcting a document over the phone. There are three ways to let a model change a file, and they work like the three ways to do that:
- Rewrite the whole file. You dictate every word again. Simple, but slow and costly for big files, and the model may drop lines it didn't mean to touch.
- Edit by line numbers. "Change line 42." Fragile: after the first edit, every line number below it shifts.
- Replace an exact string. "Find this exact sentence and replace it with this one." The model quotes the text to change (
old_string) and its replacement (new_string).
The third wins. The quote proves the model looked at the current text. The change is small and easy to review.
The rule that old_string must appear exactly once stops it editing the wrong place. If you say "fix the sentence about John" and there are three, you'll be asked which. If the text appears more often, the model adds surrounding lines to make it unique, or asks for replace_all.
text = "total = 0\ntotal = total + 1\n" print(text.count("total = "))
Show the answer
2
"total = " appears at the start of both lines, so count gives 2, and an edit using it would be ambiguous. Adding context makes it unique.
Why the match must be unique
A plain replace changes every copy. The edit tool counts first.
source = """def area(w, h): return w * h def volume(w, h, d): return w * h * d """ old = "return w * h" print("matches:", source.count(old)) old = " return w * h\n\ndef volume" print("with context:", source.count(old)) print(source.replace(old, " return round(w * h, 2)\n\ndef volume", 1))
"return w * h" also matches inside return w * h * d. Adding the next line makes the match unique, so only the intended line changes.
x = 1, which appears three times in the file. What should the edit tool do?Show the answer
Refuse, say it appears 3 times, and ask for more context or replace_all
Guessing which one was meant risks a silent wrong edit. A clear error costs one more turn.
Keep paths inside the workspace
The agent works in one folder, the workspace, like a workshop with a fence around it. Every tool must refuse paths outside it, whatever the model writes: ../../etc/passwd, /home/me/.ssh/id_rsa, or a symlink that points out. A symlink is a file that's really a shortcut to somewhere else, so it can lead outside the fence while looking like it's inside.
The reliable check:
- Join the path to the workspace root.
- Resolve it:
Path.resolve()turns..and symlinks into the real absolute path. - Check the result is still inside the root, with
is_relative_to(root).
Checking the raw string for .. isn't enough: there are too many ways to spell the same escape.
In plain words: work out where a path really leads, and only then decide whether to allow it.
Paths that try to escape
Here's the check from this section, tried on the paths a model might send, including ones that look harmless until they're resolved.
from pathlib import Path root = Path("/work/project").resolve() def safe(path): full = (root / path).resolve() return full.is_relative_to(root), str(full) for p in ["src/app.py", "src/../notes.txt", "../../etc/passwd", "/etc/passwd", "src/../../secrets.txt"]: print(p, "->", safe(p))
Note the fourth: joining with an absolute path throws away the root completely. And the second contains .. yet stays inside, so a check that only searches for .. would wrongly refuse it. Resolving first gives the real answer.
Read before you write
A painter looks at a wall before painting over it. Two rules stop the agent clobbering work:
- Read before overwrite. The harness remembers which files the agent has read. Writing or editing an existing file that wasn't read is refused. That stops the agent replacing a file it has never seen.
- No stale edits. The harness also remembers the content it read. If the file has changed since, say because the user edited it, the edit is refused with "read it again". The agent then works from the current text.
Catching a stale write
If a person edits a file after the agent read it, a blind write destroys their work. So a harness remembers what the file looked like when it was read.
import hashlib files = {"app.py": "print('v1')"} seen = {} def fingerprint(text): return hashlib.sha256(text.encode()).hexdigest()[:8] def read(path): seen[path] = fingerprint(files[path]) return files[path] def write(path, text): if path in files and seen.get(path) != fingerprint(files[path]): return "refused: read the file first, it has changed" files[path] = text seen[path] = fingerprint(text) return "written" print(write("app.py", "print('v2')")) # never read it read("app.py") files["app.py"] = "print('edited by a person')" print(write("app.py", "print('v2')")) # changed since it was read read("app.py") print(write("app.py", "print('v2')"))
The first two writes get refused, and the refusal tells the model what to do. Once it has read the current version it can write, knowing what it's replacing.
Output a model can use
Read shows line numbers, like page numbers in a book, so the model and the user can talk about "line 42". Grep shows path:line: text. Both cap their output and say when they've cut it, as you built in the registry lesson.
config.yml, which it has never read. What should the write tool do?Show the answer
Refuse, and ask it to read the file first
Read-before-write stops the agent replacing a file it has never seen, such as one with a person's recent changes.
Checks that look right and aren't
Searching for .. doesn't protect a path. It blocks notes/../plan.md, which is harmless, and it misses /etc/passwd, which contains no dots at all. It misses a symlink as well, which looks like an ordinary file name and points anywhere. Only resolving the path, and then comparing the result with your root, gives a reliable answer.
Replacing the first match isn't a safe guess. Say the model asks to replace total = 0, and the file has that line in three functions. If your tool quietly edits the first, the change lands in the wrong place, and the tests may not notice. So fail when there's more than one match, and tell the model how many there were. It can then include more surrounding lines to pin down the right one.
Blind overwrites lose work. A model that never read a file doesn't know what's in it, and it can overwrite someone's changes with its guess. Requiring a read first costs one tool call, and it prevents a whole class of accidents.
Line numbers and caps matter too. A file returned with no line numbers makes it hard for the model to refer to any spot. A file returned with no cap can fill the context in one call. Number the lines, cap the size, and say what you cut.
Practice what you just read
- 3 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 4 review cards for spaced repetition
Try a quiz question
Why do harnesses prefer exact string replacement to editing by line number?