Build your own Claude Code
Creating files·18 min

create (Write)

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage create --watch

Every read tool you built was harmless — view, glob, grep only look. This step crosses the line into side effects: create writes a brand-new file to disk, for real, inside the sandbox. It's the same Write tool the model reaches for when Claude Code scaffolds a file. Two things make a good create: it refuses to clobber an existing file, and it hands the loop back a result block clean enough to thread straight into the next turn.

Return a structured result the loop can thread back

Your tool returns a ToolOutput ({ content: string, is_error?: boolean }). The loop wraps it into a tool_result block — { type: 'tool_result', tool_use_id, content } — and feeds it straight back to the model as the next user turn. On success, return ok('created <path>'); on failure, set is_error so the model sees it went wrong and can recover.

What to build

Your task

Implement Tools.create

Implement create(path, fileText) on the Tools class. Resolve path through the sandbox guard (never trust a raw path), error if the file already exists (a fresh create must not overwrite), write fileText, and return a well-formed success result. Record the written content as the read-version anchor too — Chapter 4's edit guard will lean on it.

src/tools.ts
/** `create` — write a new file; error if it already exists. */
create(path: string, fileText: string): ToolOutput {
  const { resolved, error } = this.resolve(path);
  if (error) return err(error);
  // 1. if the file already exists -> err(`already exists: ${path}`)
  // 2. writeFileSync(resolved!, fileText)
  // 3. anchor the read-version: this.readVersions.set(resolved!, hash(fileText))
  // 4. return ok(`created ${path}`)
}
Refuse to clobber

The existence check is the whole point of create versus a blind write. If the file is already there, return an error result — err('already exists: ' + path) — and write nothing. The model that wanted to edit an existing file should be reaching for str_replace, not create; surfacing the error is how it learns that.

How it's graded

process mode, stdout_json_match. The grader runs program.ts create, which builds a temp sandbox, calls create, then reads the file back off disk and prints { result, content }. Both must match the reference exactly: the success result.content ("created new.txt") and the on-disk bytes ("hello"). The byte-compare is what proves the write actually landed.

How the grader runs it
./your_program.sh create
# case: { files: {}, path: "new.txt", text: "hello" }
# -> {"result":{"content":"created new.txt"},"content":"hello"}