Skip to content

File Write

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:

  1. locate target path (glob, list, or user instruction)
  2. read prior content when file exists
  3. produce next content in model
  4. request write permission with preview
  5. execute write
  6. 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 write tool with approval + diagnostics pipeline

  • 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 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.

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_run mode (:487-488)
  • writes with explicit encoding and newline policy: open(..., "w", encoding=self.encoding, newline=self.newline) (:493)
  • retries on PermissionError with exponential backoff (:496-500)
  • reports final failure through tool_error and re-raises (:501-504)
  • reports other OSError and 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.

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 at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476 also does not provide a dedicated write_file tool.

All file mutations run through apply_patch.

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_patch registration
  • 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.

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 } in spec.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_path for update
  • hunks with context and line prefixes

So “write file” becomes:

  • Add File for creation
  • Update File replacing body (possibly entire file)

Handler flow in tools/handlers/apply_patch.rs:80-183:

  1. parse payload (function args or custom freeform)
  2. verify parse using codex_apply_patch::maybe_parse_apply_patch_verified
  3. pass verified action to apply_patch::apply_patch(...)
  4. 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 requirement
  • AskUser -> delegate with needs-approval requirement
  • Reject -> 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 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 at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42 implements an explicit write tool in packages/opencode/src/tool/write.ts:19-85.

The schema is intentionally minimal:

  • content: string
  • filePath: 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.

Detailed sequence from write.ts:

  1. Resolve absolute path.

    filepath = isAbsolute(filePath) ? filePath : join(Instance.directory, filePath) (:26)

  2. Check external directory constraints.

    assertExternalDirectory(ctx, filepath) (:27)

    The helper (external-directory.ts:12-31) asks for external_directory permission when target is outside workspace.

  3. Inspect existing state.

    • exists = await Bun.file(filepath).exists() (:30)
    • contentOld = exists ? await file.text() : "" (:31)
    • if exists: FileTime.assert(ctx.sessionID, filepath) (:32)
  4. Compute approval diff preview.

    diff = trimDiff(createTwoFilesPatch(...)) (:34)

  5. Ask permission before mutation.

    ctx.ask({ permission: "edit", patterns, always: ["*"], metadata }) (:35-43)

    Metadata includes:

    • absolute filepath
    • synthesized unified diff preview
  6. Execute write.

    await Bun.write(filepath, params.content) (:45)

  7. Emit update events.

    • Bus.publish(File.Event.Edited, ...) (:46-48)
    • Bus.publish(FileWatcher.Event.Updated, ...) (:49-52)
    • file event type set to change or add
  8. Refresh per-session file timestamp tracking.

    FileTime.read(ctx.sessionID, filepath) (:53)

  9. Run LSP hooks and diagnostics.

    • await LSP.touchFile(filepath, true) (:56)
    • diagnostics = await LSP.diagnostics() (:57)
  10. 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)
  11. 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.

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

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.


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 turn
  • balanced: warn when overwrite exceeds N changed lines
  • fast: allow direct overwrite under existing approval grants

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.

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_parents argument
  • 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: unavailable from diagnostics: 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.


Add a dedicated write_file tool, while retaining apply_patch for minimal edits.

Proposed schema:

  • file_path: String (absolute path only)
  • content: String
  • if_exists: "overwrite" | "error" | "append" (default overwrite)
  • create_parents: bool (default false)
  • require_fresh_read: bool (default true)
  • emit_diagnostics: bool (default true)

Rationale:

  • if_exists avoids hidden overwrite semantics
  • require_fresh_read codifies stale-context guard
  • emit_diagnostics allows disabling expensive follow-up in batch mode

Response schema should be structured and stable:

  • path
  • operation: created | overwritten | appended
  • bytes_written
  • created_parent_dirs: []
  • diff_summary
  • diagnostics_status: ok | unavailable | timeout
  • diagnostics: normalized array

Phase 1: planning and approval.

  1. canonicalize and validate path
  2. enforce workspace/external boundary policy
  3. read old content if exists
  4. validate freshness token if required
  5. compute diff summary
  6. compute directory creation plan
  7. request approval with all metadata

Phase 2: execution.

  1. create parents if allowed
  2. write to temp file in same directory
  3. fsync temp file
  4. atomic rename temp -> destination
  5. emit watcher/index/LSP events
  6. capture diagnostics (optional)
  7. 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.

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

Core crates for Rust implementation:

  • tokio / tokio::fs for async file I/O
  • serde + schemars for tool schema and JSON typing
  • thiserror for typed write errors
  • similar for diff summary generation
  • notify or existing internal watcher abstraction for update events
  • sha2 (or existing hashing utility) for freshness tokening

Potential internal trait split:

  • WritePreflight: validates path, computes preview, gathers approval metadata
  • WriteExecutor: performs atomic commit, event emission, diagnostics collection
  • WritePolicy: 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.