File Write
Feature Definition
Section titled “Feature Definition”A file write capability gives the model a direct way to create a file or replace an existing file with complete new contents.
In agent systems this is the opposite end of the edit spectrum from narrow patch operations.
Patch operations optimize for minimal edits.
Full write operations optimize for certainty when the desired output is easier to regenerate than to patch.
Typical cases:
- creating a new config file from scratch
- generating a test fixture with deterministic content
- replacing templated/generated files
- writing migration scripts where the entire file is produced in one turn
- replacing long prose documents where patch churn would be larger than a rewrite
The hard part is not serialization.
The hard part is behavioral safety and reviewability.
A write operation can atomically destroy prior file content.
If path resolution is wrong, if approval metadata is weak, or if stale context is used, the model can overwrite critical sources quickly.
A strong file write design therefore needs explicit invariants.
Invariant set used by mature implementations:
- path is canonicalized before mutation
- policy checks run before write
- caller sees a concrete before/after diff or summary before approval
- existing-file writes can be guarded by freshness checks
- writes publish events so LSP/index/watcher subsystems react immediately
- output reports what happened in machine- and human-readable form
Write tools also sit in a larger workflow.
Common sequence:
- locate target path (
glob,list, or user instruction) - read prior content when file exists
- produce next content in model
- request write permission with preview
- execute write
- run diagnostics and surface errors
Without step 2, models frequently regress behavior by replacing files blindly.
Without step 4, humans cannot evaluate risk at approval time.
Without step 6, write success can mask immediate compile or lint breakage.
Another design tension is write granularity.
A dedicated write_file tool is simple for models,
but it duplicates capability available via patch languages.
Some systems deliberately avoid dedicated write tools and force all mutation through patch semantics.
That choice reduces surface area, but increases model prompt complexity for trivial full-file creation.
The three reference projects show all major patterns:
- Aider: no standalone write tool; writes happen inside coder pipelines
- Codex: no standalone write tool; patch tool is the universal mutator
- OpenCode: explicit
writetool with approval + diagnostics pipeline
See Also
Section titled “See Also”- This page is canonical for full-file create/overwrite behavior.
- For patch grammar and fuzzy matching internals, see File Edit and Edit Formats.
- For approval UX and persisted allow rules, see Approval Flow.
Aider Implementation
Section titled “Aider Implementation”Aider at commit b9050e1d5faf8096eae7a46a9ecc05a86231384b
does not expose a JSON/function-calling write_file tool.
Write behavior is embedded in coder implementations that parse model output formats and apply edits programmatically.
Write Path in Coder Apply Pipelines
Section titled “Write Path in Coder Apply Pipelines”The practical write path depends on edit format.
Two concrete flows illustrate the model.
Flow A: whole-file coder.
In aider/coders/wholefile_coder.py:124-129,
apply_edits() iterates extracted edits,
joins generated lines,
and calls self.io.write_text(full_path, new_lines).
This is the most direct full-write path.
The coder does not emit a standalone tool call.
It writes during post-response apply stage.
Flow B: patch coder.
In aider/coders/patch_coder.py:549-700,
apply_edits() handles ADD, DELETE, and UPDATE actions.
For ADD:
- checks target does not already exist (
:566-570) - creates parent directories (
:575) - enforces trailing newline if missing (
:576-579) - writes via
self.io.write_text(full_path, content_to_write)(:580)
For UPDATE:
- reads current content
- computes new content by applying parsed chunks (
_apply_update) - writes target content via
self.io.write_text(target_full_path, new_content)(:623) - unlinks original file if this was a move (
:626-627)
For DELETE:
- unlinks path directly when present (
:589)
This means “write” in Aider is an internal side effect of parsed edit actions, not a first-class model tool API.
A third related path exists in the command layer.
/add can create missing files when no pattern matches.
In aider/commands.py:838-843,
Aider asks user confirmation,
runs fname.parent.mkdir(parents=True, exist_ok=True),
then fname.touch().
That is file creation, but user-command driven, not model-driven tool execution.
Core Write Primitive: InputOutput.write_text
Section titled “Core Write Primitive: InputOutput.write_text”The shared write primitive is InputOutput.write_text
in aider/io.py:478-507.
Behavior details:
- accepts
filename,content,max_retries=5,initial_delay=0.1 - returns immediately in
dry_runmode (:487-488) - writes with explicit encoding and newline policy:
open(..., "w", encoding=self.encoding, newline=self.newline)(:493) - retries on
PermissionErrorwith exponential backoff (:496-500) - reports final failure through
tool_errorand re-raises (:501-504) - reports other
OSErrorand re-raises (:505-507)
Important implication:
Aider centralizes retry and encoding policy in one function, so all coder mutation paths share consistent file-write behavior.
But there is no pre-write policy gate specific to this primitive.
Permission/approval is handled outside, at command flow or agent confirmation layers.
No Standalone Write Tool
Section titled “No Standalone Write Tool”Consequences of the Aider architecture:
- model cannot emit a typed write call with strict schema
- no per-write JSON arguments like
if_exists,create_parents,mode - no dedicated write result payload with stable metadata contract
- no isolated write permission category separate from edit workflows
- write semantics depend on whichever coder/edit format is active
Operationally this works because Aider’s interaction style is “model emits edit text, Python code applies it.”
But for tool-native agents, this couples write behavior to parsing logic instead of exposing explicit mutation contracts.
Codex Implementation
Section titled “Codex Implementation”Codex at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476
also does not provide a dedicated write_file tool.
All file mutations run through apply_patch.
No Dedicated write_file Tool
Section titled “No Dedicated write_file Tool”ToolsConfig in codex-rs/core/src/tools/spec.rs:35-45
contains apply_patch_tool_type
and experimental_supported_tools,
but no dedicated write tool toggle.
Tool registration in spec.rs:1499-1537 shows:
- optional
apply_patchregistration - optional
grep_files - optional
read_file - optional
list_dir
No standalone file-write function is registered.
This is intentional surface minimization.
One mutator.
Many readers/searchers.
apply_patch as Write Mechanism
Section titled “apply_patch as Write Mechanism”Codex supports two apply_patch shapes.
Freeform variant:
- created by
create_apply_patch_freeform_tool() - grammar source:
tool_apply_patch.lark - suited to GPT-5 custom tool flow
Function variant:
- created by
create_apply_patch_json_tool() - schema struct
ApplyPatchToolArgs { input: String }inspec.rs:1203-1207 - used for models needing JSON function arguments
Patch grammar (tool_apply_patch.lark:1-19) supports:
*** Add File: path*** Update File: path*** Delete File: path- optional
*** Move to: new_pathfor update - hunks with context and line prefixes
So “write file” becomes:
Add Filefor creationUpdate Filereplacing body (possibly entire file)
Handler flow in tools/handlers/apply_patch.rs:80-183:
- parse payload (function args or custom freeform)
- verify parse using
codex_apply_patch::maybe_parse_apply_patch_verified - pass verified action to
apply_patch::apply_patch(...) - either:
- return direct output (already handled)
- or delegate to runtime/orchestrator for execution
Safety pre-check lives in core/src/apply_patch.rs:36-75.
assess_patch_safety(...) yields:
AutoApprove-> delegate with skip-approval requirementAskUser-> delegate with needs-approval requirementReject-> return error to model
Runtime execution in tools/runtimes/apply_patch.rs:49-160:
- builds self-invocation command
codex --codex-run-as-apply-patch <patch> - runs with minimal environment (
env: HashMap::new()) - uses orchestrator sandbox attempt logic
- reuses cached approvals by file path keys
This architecture means write/create is not a separate execution primitive.
It is a patch action class inside a single mutation runtime.
Approval and Path Resolution
Section titled “Approval and Path Resolution”Approval key extraction is explicit.
file_paths_for_action() in apply_patch.rs:39-57 collects:
- original changed paths
- move destinations for rename updates
This prevents approval blind spots during rename writes.
The helper uses absolute path resolution (to_abs_path) and
includes destination path when move_path exists (:48-53).
If parsing fails, Codex responds with explicit model-facing errors:
- unsupported payload
- invalid patch input
- non-apply-patch input
- correctness verification failures
intercept_apply_patch() in apply_patch.rs:187-263
adds a guardrail for cases where the model tries to run patch text
via shell/exec tool.
Codex warns and routes toward the dedicated apply_patch path.
Result shaping:
- Tool events emitted through
ToolEmitter::apply_patch - output returned as
FunctionCallOutputBody::Text - success flag attached when execution succeeds
Write semantics are therefore robust, but mediated by patch correctness and patch safety.
There is no mode where the model sends raw full text and path without patch envelope.
OpenCode Implementation
Section titled “OpenCode Implementation”OpenCode at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42
implements an explicit write tool
in packages/opencode/src/tool/write.ts:19-85.
Parameter Schema (Zod)
Section titled “Parameter Schema (Zod)”The schema is intentionally minimal:
content: stringfilePath: string
from write.ts:21-24.
The tool description file write.txt:1-8 adds policy guidance,
including:
- overwrite behavior is expected
- read existing file first when updating
- prefer editing existing files over creating new files
- avoid proactive docs creation unless explicitly requested
In code, freshness enforcement is handled via FileTime.assert
for existing files (write.ts:32).
This is how OpenCode operationalizes “read before overwrite” semantics.
Execution Flow
Section titled “Execution Flow”Detailed sequence from write.ts:
-
Resolve absolute path.
filepath = isAbsolute(filePath) ? filePath : join(Instance.directory, filePath)(:26) -
Check external directory constraints.
assertExternalDirectory(ctx, filepath)(:27)The helper (
external-directory.ts:12-31) asks forexternal_directorypermission when target is outside workspace. -
Inspect existing state.
exists = await Bun.file(filepath).exists()(:30)contentOld = exists ? await file.text() : ""(:31)- if exists:
FileTime.assert(ctx.sessionID, filepath)(:32)
-
Compute approval diff preview.
diff = trimDiff(createTwoFilesPatch(...))(:34) -
Ask permission before mutation.
ctx.ask({ permission: "edit", patterns, always: ["*"], metadata })(:35-43)Metadata includes:
- absolute filepath
- synthesized unified diff preview
-
Execute write.
await Bun.write(filepath, params.content)(:45) -
Emit update events.
Bus.publish(File.Event.Edited, ...)(:46-48)Bus.publish(FileWatcher.Event.Updated, ...)(:49-52)- file event type set to
changeoradd
-
Refresh per-session file timestamp tracking.
FileTime.read(ctx.sessionID, filepath)(:53) -
Run LSP hooks and diagnostics.
await LSP.touchFile(filepath, true)(:56)diagnostics = await LSP.diagnostics()(:57)
-
Append diagnostic feedback to tool output.
- same-file errors first (
:66-68) - project-file errors capped by
MAX_PROJECT_DIAGNOSTICS_FILES = 5(:17,:70-72) - per-file cap
MAX_DIAGNOSTICS_PER_FILE = 20(:16,:63-65)
- same-file errors first (
-
Return structured payload.
title: path relative to worktree (:76)metadata: diagnostics, filepath,exists(:77-81)output: success message + diagnostics block (:55-83)
This is a classic preflight-then-commit write architecture.
Permission Request Shape
Section titled “Permission Request Shape”Write permission call shape (write.ts:35-43):
permission: "edit"patterns: [relativePath]always: ["*"]metadata: { filepath, diff }
Important details:
- approval includes semantic preview (
diff), not only path - pattern is relative to worktree
- once user grants persistent approval, wildcard policy can auto-approve subsequent edits
External directory access is separated from edit permission
through assertExternalDirectory.
That yields cleaner policy expression:
- one check for path boundary crossing
- one check for content mutation intent
Post-Write Diagnostics and Metadata
Section titled “Post-Write Diagnostics and Metadata”OpenCode writes are tightly integrated with diagnostics.
After write:
- LSP is touched for the edited file
- diagnostic map is queried
- severity filter keeps only error-level diagnostics (
severity === 1) - both same-file and other-file failures can be surfaced
Output is intentionally instructional, not only declarative.
When errors exist, tool output asks model to fix them and embeds XML-style diagnostics blocks.
Metadata captures full diagnostics object, so UI layers can render rich views without parsing free text.
This design reduces follow-up latency.
The model gets write result and failure context in the same turn.
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons”Full Overwrite Is High Blast Radius
Section titled “Full Overwrite Is High Blast Radius”A full overwrite can accidentally erase nuanced logic that a patch would have preserved.
Common failure patterns:
- model synthesizes a “clean” file and drops edge-case branches
- missing imports from adjacent refactors are lost
- comments/docstrings with operational constraints disappear
- generated headers/licenses are removed
Mitigations used in reference systems:
- diff-before-approval metadata (OpenCode)
- read-before-write freshness assertion (OpenCode
FileTime.assert) - single mutator path with strong patch verification (Codex)
- coder-level validation and explicit error throw (Aider patch coder)
For OpenOxide, blast radius should be configurable.
Suggested policy levels:
strict: deny overwrite unless previous read happened this turnbalanced: warn when overwrite exceeds N changed linesfast: allow direct overwrite under existing approval grants
Diff-Before-Ask Is Crucial for UX
Section titled “Diff-Before-Ask Is Crucial for UX”Approval prompts that only say “write file X?” force blind trust.
Prompts with compact diffs let users make fast, accurate decisions.
Design guidance:
- include changed line count summary
- include top-level file classification (new file, overwrite, rename target)
- include truncation notice if diff preview is clipped
- include path normalization result so user sees final destination
OpenCode’s trimDiff(createTwoFilesPatch(...))
is the baseline pattern.
Codex’s patch visualization through event pipeline provides equivalent clarity at tool layer.
Parent Directory Semantics Need Policy
Section titled “Parent Directory Semantics Need Policy”Auto-creating parent directories is convenient, but can silently create wrong folder trees from minor path hallucinations.
Observed bad outcomes:
- typo in
src/servre/creates parallel subtree - case mismatch on case-sensitive systems
- nested temp directories created from malformed vars
Mitigations:
- explicit
create_parentsargument - approval prompt highlights any new directories created
- optional policy: reject creation beyond one missing parent level
- workspace boundary check before directory creation
Aider’s /add flow currently creates parents on confirmation.
Patch paths in Codex/OpenCode also create parents as a side effect of file operations.
OpenOxide should expose this behavior as explicit contract, not hidden implementation detail.
Diagnostics Are Advisory, Not Transactional
Section titled “Diagnostics Are Advisory, Not Transactional”Post-write diagnostics improve loop speed, but they do not make writes safe by themselves.
Failure mode:
- write succeeds
- diagnostics fail (timeout/transport crash)
- tool returns partial state
- model assumes no diagnostics means no errors
Mitigations:
- include diagnostics execution status in result metadata
- distinguish
diagnostics: unavailablefromdiagnostics: empty - keep write operation idempotent regardless of diagnostics outcome
- never roll back write automatically solely because diagnostics failed to run
OpenCode already treats diagnostics as additive feedback, not commit gate.
Codex similarly separates mutation approval from post tools.
OpenOxide should follow same separation.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Tool Contract
Section titled “Tool Contract”Add a dedicated write_file tool,
while retaining apply_patch for minimal edits.
Proposed schema:
file_path: String(absolute path only)content: Stringif_exists: "overwrite" | "error" | "append"(defaultoverwrite)create_parents: bool(defaultfalse)require_fresh_read: bool(defaulttrue)emit_diagnostics: bool(defaulttrue)
Rationale:
if_existsavoids hidden overwrite semanticsrequire_fresh_readcodifies stale-context guardemit_diagnosticsallows disabling expensive follow-up in batch mode
Response schema should be structured and stable:
pathoperation:created | overwritten | appendedbytes_writtencreated_parent_dirs: []diff_summarydiagnostics_status:ok | unavailable | timeoutdiagnostics: normalized array
Two-Phase Write
Section titled “Two-Phase Write”Phase 1: planning and approval.
- canonicalize and validate path
- enforce workspace/external boundary policy
- read old content if exists
- validate freshness token if required
- compute diff summary
- compute directory creation plan
- request approval with all metadata
Phase 2: execution.
- create parents if allowed
- write to temp file in same directory
- fsync temp file
- atomic rename temp -> destination
- emit watcher/index/LSP events
- capture diagnostics (optional)
- return structured output
Why same-directory temp file:
- preserves atomic rename on POSIX
- avoids cross-device rename failures
- reduces race conditions around partial writes
Fresh-read guard proposal:
- store
(path, mtime, hash, session_id, turn_id)on read - write checks current file identity
- if changed since read, return conflict error unless force flag provided
This mirrors optimistic concurrency control and prevents stale overwrites in long turns.
Atomicity and Rollback
Section titled “Atomicity and Rollback”Write should be atomic at file level by default.
For multi-file workflows, rollback strategy should be explicit.
Suggested levels:
- single-file tool call: atomic by temp+rename
- multi-file batched tool call:
- pre-snapshot touched files
- apply sequential writes
- on failure, restore prior contents for already-written files
Rollback metadata should include:
- files restored
- files skipped
- root cause
Do not silently swallow partial failure.
Return a typed partial-failure object.
Codex’s apply_patch runtime demonstrates separating approval from execution engine.
OpenOxide can apply the same orchestrator contract for write runtime.
Integration points to build from day one:
- file event bus notifications
- LSP touch + diagnostics query
- turn diff tracker updates
- permission cache keys per absolute file path
Crates
Section titled “Crates”Core crates for Rust implementation:
tokio/tokio::fsfor async file I/Oserde+schemarsfor tool schema and JSON typingthiserrorfor typed write errorssimilarfor diff summary generationnotifyor existing internal watcher abstraction for update eventssha2(or existing hashing utility) for freshness tokening
Potential internal trait split:
WritePreflight: validates path, computes preview, gathers approval metadataWriteExecutor: performs atomic commit, event emission, diagnostics collectionWritePolicy: resolves overwrite/freshness/parent rules from config and mode
Testing matrix should include:
- create vs overwrite vs append
- missing parent with/without
create_parents - stale read conflict
- external directory denial path
- diagnostics unavailable path
- concurrent write race in same session
- permission cache hit/miss semantics
A write tool is deceptively simple on paper.
In production, quality comes from preflight rigor, explicit policy, and predictable post-write feedback.