File Tools — Read, Edit, Write

The three primary file tools. When to use each, the read-before-edit invariant, and why Edit is preferred over Write for existing files.

Feature Foundational
8 min read
file-tools read edit write conventions

What it is#

Read, Edit, and Write are Claude Code’s three primary file-manipulation tools. Together they cover everything the model does to files inside a session: pulling content into context, modifying it surgically, or rewriting it wholesale.

  • Read opens a file and returns its contents (with line numbers) to the model. It is read-only, cheap, and the canonical way to bring source into context.
  • Edit performs an exact string replacement inside an existing file. It is the default tool for any change to a file that already exists.
  • Write creates a file from scratch or replaces an existing one in full. It is the tool of last resort when Edit cannot express the change.

These three tools are deliberately small and specialised. The model picks between them based on intent, and the harness enforces invariants that prevent the common failure modes — most importantly the read-before-edit invariant that makes blind edits impossible.

When to use it#

The decision tree is short and worth committing to muscle memory.

  • Read when you need to know what is in a file before changing it, or when the user asks a question that requires looking at code. Reading is also how you verify an assumption — never edit a file you have not just read.
  • Edit when an existing file needs a localised change: fixing a bug, renaming a symbol, adding a function, tweaking a constant. Edit preserves everything outside the matched string, which protects unrelated code from accidental drift.
  • Write when there is no existing file, or when the rewrite touches so much of the file that a series of edits would be slower and noisier than starting fresh. Write is also right when migrating a file to a new format (e.g. converting a config file to a new schema).

Avoid Write for files that already exist and have unrelated content you care about. The risk is silently dropping a section, a comment, or a config block that the model did not realise was load-bearing. The cost of an extra Edit call is much smaller than the cost of an undetected regression.

How it works#

Read#

Read takes an absolute path and returns numbered lines. The numbers are not part of the file — they are added to make the model’s downstream Edit calls easier to reason about. Read can also page large files: pass offset and limit to read a slice, which matters when a file is too big to fit comfortably in context.

Reading the same file twice in a session is allowed but wasteful. Once a file is in context, the model already knows its contents; re-reading is the right move only if you suspect the file has changed on disk since the last read (e.g. after a Bash command that edits files).

Edit#

Edit takes three arguments: the file path, the old_string to find, and the new_string to replace it with. The match is exact — whitespace, tabs, newlines, all of it. Two invariants make Edit reliable:

  1. Read-before-edit. The harness rejects an Edit call against a file the model has not read in the current session. This forces the model to know what it is changing.
  2. Uniqueness. The old_string must occur exactly once in the file. If it appears multiple times, the call fails with a helpful error and the model has to disambiguate by widening the match with more surrounding context.

The replace_all option overrides the uniqueness check and replaces every occurrence — useful for symbol renames where you genuinely want every instance.

Write#

Write takes a path and the full file contents. If the file exists, the harness still requires a prior Read so the model has seen the current contents before overwriting them. The whole file gets replaced atomically; there is no partial-write state.

Configuration#

There is no configuration for the file tools themselves — they are always available. What you can configure is which paths the model may touch, via the permissions system. A project that wants to prevent edits under migrations/ or .env* can express that in settings.json:

{
"permissions": {
"deny": [
"Edit(migrations/**)",
"Write(migrations/**)",
"Edit(.env*)",
"Write(.env*)"
]
}
}

The deny rules are evaluated before the tool runs; a denied Edit returns an error and the model has to find another path. Pair this with a CLAUDE.md line explaining the why so the model knows to suggest a different approach rather than re-attempt the same call.

The harness also tracks per-file read state across the session. Once read, a file stays “known” until the session ends — so a Read followed twenty turns later by an Edit still satisfies the read-before-edit check.

Examples#

Read a slice of a large file#

When a log file or a generated artifact is too big to fit in context, page it.

{
"tool": "Read",
"file_path": "/var/log/app.log",
"offset": 1200,
"limit": 200
}

This returns lines 1200–1399 with line numbers. Combine with grep to find the slice you want first, then Read that slice.

Edit a single occurrence#

The default mode. Note the surrounding context in old_string — enough to be unique, not so much that whitespace becomes a hazard.

// Tool call shape
{
tool: "Edit",
file_path: "/repo/src/api/auth.ts",
old_string: " const TIMEOUT_MS = 5000;",
new_string: " const TIMEOUT_MS = 30000;"
}

Rename a symbol everywhere#

When the symbol appears in many places and you want them all changed, use replace_all.

{
tool: "Edit",
file_path: "/repo/src/lib/queue.ts",
old_string: "enqueueTask",
new_string: "scheduleTask",
replace_all: true
}

This is faster than dozens of individual Edits — but be sure the name is genuinely unique to the symbol you are renaming. A common pitfall is renaming something like User and catching UserAgent in the blast radius.

Write a new file#

Creating a fixture, a config, or a new module.

# Tool call shape
{
"tool": "Write",
"file_path": "/repo/tests/fixtures/users.json",
"content": "[{\"id\": 1, \"name\": \"Alice\"}, {\"id\": 2, \"name\": \"Bob\"}]\n"
}

Edit followed by verification#

A common pattern: edit, then run the test that exercises the change. The Edit + Bash pairing is the unit of progress.

Terminal window
# After the Edit returns success
$ pnpm test -- auth

Gotchas#

  • Read-before-edit is enforced. A common confusion early on is “why won’t the Edit go through?” — almost always the answer is the file has not been read in this session. Read first, edit second.
  • Whitespace mismatches kill Edits. If you copy old_string from a code snippet that used spaces but the file uses tabs, the match fails. Use the exact text from Read’s output.
  • Edit’s uniqueness check is unforgiving. If old_string appears twice, the call fails. Either widen the context or use replace_all. Do not try to disambiguate with regex — Edit is exact-match only.
  • Write overwrites silently. There is no merge. If you Write a file that has unrelated content, that content is gone. Re-read recent files before Writing on top of them.
  • Line numbers are not in the file. Read prefixes every line with <n>\t<content>. When constructing old_string for Edit, strip the number prefix or the match will fail. The harness reminds you of this but it is still the single most common Edit failure.
  • Edit cannot insert at end-of-file cleanly without context. Either widen old_string to include the last few real lines, or Write the file fresh if the change is large.
  • Long files: read once, edit many. Each Read of a 5000-line file costs tokens. Read it once, do the series of Edits, move on. Re-reading on every turn is the path to burning context for no reason.
  • The model can be tempted to Write when Edit is right. If the model proposes a Write for an existing file, ask why — Write is almost always the wrong call when Edit would do.

Edit vs Write at a glance#

Edit
  • Targeted, localised change
  • Preserves everything outside the match
  • Cheap on tokens (sends only the diff)
  • Hard to get wrong: uniqueness check, read invariant
  • Series of small Edits beats one big Write almost always
Write
  • Whole-file replacement
  • Right for new files or large rewrites
  • Expensive on tokens (sends full content)
  • Easy to lose nearby content if used carelessly
  • Reach for it last, not first
Why Edit + Read beats 'just rewrite the file'

A whole-file rewrite is what most chat-only assistants do, and it has two costs: the model has to regenerate every line of the file (expensive in tokens, slow), and the diff that hits version control is gigantic and noisy (impossible to review meaningfully). A targeted Edit produces a tight diff that says exactly what changed — which is the diff a reviewer actually wants. Once you internalise that, the choice between Edit and Write stops being a style question and becomes a craft one.

Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.