Index Ai Academy
Phase 10 · Agent harnesses · Tools & environment

File and search tools

Read, write, edit by exact replacement, glob and grep inside one folder.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. The five tools
  2. Edit by exact replacement
  3. Why the match must be unique
  4. Keep paths inside the workspace
  5. Paths that try to escape
  6. Read before you write
  7. Catching a stale write
  8. Output a model can use
  9. 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:

tooldoeskey design choices
readreturns lines of a fileline numbers in the output; offset and limit for big files
writecreates or replaces a whole filefor existing files, only after reading them
editreplaces an exact string in a filethe old text must match exactly once
globfinds files by name pattern (src/**/*.py)sorted, capped results
grepfinds lines matching a regexpath: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.

Try it now

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:

  1. 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.
  2. Edit by line numbers. "Change line 42." Fragile: after the first edit, every line number below it shifts.
  3. 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.

Quick check
Why must an edit's old text be unique? Count the matches. What does this print?
text = "total = 0\ntotal = total + 1\n"
print(text.count("total = "))
  1. 2
  2. 3
  3. 1
  4. 0
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.

Worked example

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))
It runs right here in your browser, with nothing to install.

"return w * h" also matches inside return w * h * d. Adding the next line makes the match unique, so only the intended line changes.

Quick check
The model asks to replace x = 1, which appears three times in the file. What should the edit tool do?
  1. Replace all three
  2. Replace the first one
  3. Replace the last one
  4. Refuse, say it appears 3 times, and ask for more context or replace_all
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:

  1. Join the path to the workspace root.
  2. Resolve it: Path.resolve() turns .. and symlinks into the real absolute path.
  3. 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.

Worked example

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))
In the lesson you can change this and run it.

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.
Worked example

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')"))
In the lesson you can change this and run it.

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.

Quick check
The agent tries to overwrite config.yml, which it has never read. What should the write tool do?
  1. Refuse, and ask it to read the file first
  2. Overwrite it
  3. Append to the end instead
  4. Rename the old file and write a new one silently
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

Create a free account

Try a quiz question

Why do harnesses prefer exact string replacement to editing by line number?

  1. Line numbers shift after each edit, while a quoted string proves the model saw the current text
  2. Line numbers are slower to compute, because the harness has to scan the whole file for each one
  3. Models can't count lines, so any line number they give is likely to be off by a few in a long file
  4. Files don't have line numbers, and adding them would change the text that the model is meant to edit

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson