write
Create or overwrite a file, writable internal resource, archive entry, SQLite row, or merge-conflict resolution.
Source
- Entry:
packages/coding-agent/src/tools/write.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/write.md - Key collaborators:
packages/coding-agent/src/tools/archive-reader.ts— parsearchive.ext:entryselectors.packages/coding-agent/src/tools/sqlite-reader.ts— detect SQLite paths and perform row insert/update/delete.packages/coding-agent/src/lsp/index.ts— format-on-write and diagnostics writethrough.packages/coding-agent/src/tools/auto-generated-guard.ts— block overwriting generated files.packages/coding-agent/src/tools/fs-cache-invalidation.ts— invalidate shared FS scan caches after writes.packages/coding-agent/src/tools/plan-mode-guard.ts— resolve paths and enforce plan-mode write policy.
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
path |
string |
Yes | Target path. Plain file path writes a filesystem file. Writable internal URLs are delegated to their handler. archive.ext:inner/path writes an archive entry for .tar, .tar.gz, .tgz, or .zip. db.sqlite:table inserts a row. db.sqlite:table:key updates or deletes a row. conflict://<id> resolves a recorded merge conflict. |
content |
string |
Yes | Full replacement file content, archive entry content, internal-resource content, conflict replacement, or SQLite row payload. SQLite non-delete writes must parse as a JSON5 object. Empty or whitespace-only content deletes a SQLite row when path includes a row key. |
Worked examples:
path: "src/generated/config.json"
content: "{\n \"enabled\": true\n}\n"
path: "fixtures/archive.zip:templates/email.txt"
content: "hello\n"
path: "data/app.sqlite:users:42"
content: "{name: 'Ada', active: true}"
Outputs
Single-shot result.
- Success always returns a text block.
- Plain file write:
Successfully wrote <chars> bytes to <relative-path>(the count iscleanContent.length, not encoded byte length). - Internal URL write:
Successfully wrote <chars> bytes to <url>. - Archive write:
Successfully wrote <chars> bytes to <relative-archive-path>:<entry-path>. - SQLite write: one of
Inserted row into <table>,Updated row '<key>' in <table>,No row updated ...,Deleted row ...,No row deleted .... - Conflict resolution: conflict-specific success text, with fresh hashline snapshot headers when applicable.
- Plain file write:
- If hashline prefixes were copied from
readoutput and stripped first, the first text block gets an extra note. - In hashline display mode, plain file writes (including ACP bridge writes) and conflict resolutions prepend a fresh
¶<relative-path>#TAGheader so the nextedithas a current snapshot tag without an extraread. Bulk conflict resolutions append aSnapshots:block listing one header per successfully written file. - Plain file writes may also return
details.diagnosticsplusdetails.meta.diagnosticswhen LSP diagnostics-on-write is enabled, anddetails.madeExecutablewhen a newly written shebang file is chmodded executable. - SQLite writes use
toolResult(...).sourcePath(...), sodetails.meta.sourcePathpoints at the database file. - Archive and internal URL writes return empty
details.
Flow
WriteTool.execute()inpackages/coding-agent/src/tools/write.tsstrips pasted¶PATH#HASHheaders andLINE:hashline prefixes fromcontentwhen the session is in hashline display mode.- If
pathis an internal URL whose handler exposeswrite, the tool delegates directly tohandler.write(...)and returns. conflict://...paths are handled next by the merge-conflict resolver. Scope reads such asconflict://<id>/oursare rejected as read-only; writable conflict URIs must omit the scope.- It calls
#resolveArchiveWritePath()next. That usesparseArchivePathCandidates()frompackages/coding-agent/src/tools/archive-reader.ts, checks candidate archive files on disk, and falls back to the longest matching archive suffix even when the archive file does not exist yet. - Archive writes call
enforcePlanModeWrite(..., { op: exists ? "update" : "create" }), then#writeArchiveEntry().- The parent directory of the archive file is created with
fs.mkdir(..., { recursive: true }). .ziparchives are read withfflate.unzipSync(), the target entry is replaced in an in-memory map, and the archive is rewritten withfflate.zipSync()+Bun.write()..tar,.tar.gz, and.tgzarchives are read withBun.Archive, existing entries are copied into an object map, the target entry is replaced, andBun.Archive.write()rewrites the archive.invalidateFsScanAfterWrite()runs on the archive file path.
- The parent directory of the archive file is created with
- If the path is not treated as an archive,
execute()calls#resolveSqliteWritePath(). That usesparseSqlitePathCandidates()andisSqliteFile()frompackages/coding-agent/src/tools/sqlite-reader.ts. Existing non-SQLite files suppress the SQLite path interpretation. - SQLite writes call
enforcePlanModeWrite(..., { op: "update" }), then#writeSqliteRow().- The database must already exist; missing DBs throw
SQLite database '<path>' not found. - The tool opens
new Database(..., { create: false, strict: true })and setsPRAGMA busy_timeout = 3000. - Whitespace-only
contentwith a row key deletes a row. - Non-empty
contentis parsed withBun.JSON5.parse(), must be a JSON object, and is routed to insert/update helpers frompackages/coding-agent/src/tools/sqlite-reader.ts. invalidateFsScanAfterWrite()runs on the DB path and the connection is closed infinally.
- The database must already exist; missing DBs throw
- Otherwise the tool treats
pathas a plain filesystem file.enforcePlanModeWrite(..., { op: "create" })runs before path resolution.- Existing files are checked by
assertEditableFile()to block overwriting detected generated files. - ACP bridge writeTextFile is tried first when available; otherwise the session’s writethrough callback writes content. With LSP enabled and
lsp.formatOnWrite/lsp.diagnosticsOnWritesettings on,createLspWritethrough()may format content, sync it through LSP servers, save it, and collect diagnostics. OtherwisewritethroughNoop()writes directly withBun.write()orfile.write(). maybeMarkExecutableForShebang()may chmod the file executable when content starts with#!.invalidateFsScanAfterWrite()runs on the file path.
- The tool returns a text result and optional diagnostics / executable metadata.
Modes / Variants
Plain file path
- Target is any path that does not resolve as an archive selector and does not resolve as an existing-or-new SQLite selector.
- Existing files are overwritten.
write.tsdoes not callfs.mkdir()on this path; parent-directory creation is only implemented in the archive branch.
Example:
path: "tmp/output.txt"
content: "hello\n"
Archive entry write
- Selector syntax:
archive.ext:inner/path. - Supported archive suffixes come from
parseArchivePathCandidates():.tar,.tar.gz,.tgz,.zip. - The inner path is normalized to
/, strips empty and.segments, rejects.., and rejects directory targets ending in/. - Rewrites the whole archive file after replacing one entry.
- Creates the parent directory for the archive file if needed.
Example:
path: "build/assets.tar.gz:css/app.css"
content: "body { color: black; }\n"
SQLite table insert
- Selector syntax:
db.sqlite:table. contentmust parse as a JSON5 object.- Empty object is allowed and becomes
INSERT INTO <table> DEFAULT VALUES. - Query parameters are rejected for SQLite writes.
Example:
path: "data/app.db:users"
content: "{name: 'Ada', active: true}"
SQLite row update / delete
- Selector syntax:
db.sqlite:table:key. - Non-empty
contentupdates the row. - Empty or whitespace-only
contentdeletes the row. - Row lookup uses the single-column primary key if present; otherwise it falls back to
rowid. Composite primary keys andWITHOUT ROWIDtables are rejected for key-based writes.
Example update:
path: "data/app.sqlite:users:42"
content: "{email: 'ada@example.com'}"
Example delete:
path: "data/app.sqlite:users:42"
content: ""
Side Effects
- Filesystem
- Creates or overwrites plain files.
- Rewrites entire archive files when writing an archive entry.
- Creates parent directories for archive files only.
- Mutates existing SQLite databases; never creates a new SQLite DB.
- Resolves conflict markers in files for
conflict://...writes. - May chmod a shebang file executable after a successful plain-file write.
- Subprocesses / native bindings
- Uses Bun SQLite bindings via
bun:sqlite. - Uses Bun archive APIs and lazily imports
fflatefor ZIP reads/writes. - May talk to configured LSP servers through
packages/coding-agent/src/lsp/index.ts.
- Uses Bun SQLite bindings via
- Session state (transcript, memory, jobs, checkpoints, registries)
- Invalidates shared filesystem scan cache entries through
invalidateFsScanAfterWrite(). - Enforces plan-mode write restrictions before mutating the target.
- Invalidates shared filesystem scan cache entries through
- Background work / cancellation
- Marks the tool
nonAbortable = trueandconcurrency = "exclusive"inWriteTool. - LSP writethrough can schedule deferred diagnostics fetches after a timeout, but plain
write.tsonly consumes the immediate return value.
- Marks the tool
Limits & Caps
WriteToolitself exposes no byte cap beyond storingcontentin memory and, for archives, rebuilding the archive in memory.- Generated-file detection reads at most
CHECK_BYTE_COUNT = 1024bytes andHEADER_LINE_LIMIT = 40header lines from an existing file inpackages/coding-agent/src/tools/auto-generated-guard.ts. - SQLite writes set
PRAGMA busy_timeout = 3000. - LSP writethrough uses a
5_000ms operation timeout inrunLspWritethrough()and may schedule a deferred diagnostics fetch withAbortSignal.timeout(25_000)inscheduleDeferredDiagnosticsFetch(). - Shebang executable handling depends on host filesystem chmod support.
Errors
- Invalid archive subpaths throw
ToolErrorwith messages such as:Archive write path must target a file inside the archiveArchive write path must target a file, not a directoryArchive path cannot contain '..'
- SQLite path parsing throws on unsupported forms:
SQLite write paths do not support query parametersSQLite write path must target a tableSQLite row writes require a non-empty row key
- Missing SQLite DBs surface as
SQLite database '<path>' not found. - SQLite content errors are model-visible
ToolErrors, including invalid JSON5, non-object payloads, unknown columns, non-scalar values, empty update objects, composite primary keys, andWITHOUT ROWIDtables. - Existing plain files may be rejected by
assertEditableFile()when they look generated. - Conflict scope writes such as
conflict://<id>/oursare rejected as read-only; invalid conflict IDs or missing conflict history surface asToolErrors from the conflict resolver. - Archive read/write failures and unexpected SQLite exceptions are wrapped in
ToolError(error.message). - If no LSP server matches or LSP formatting/diagnostics times out, file writes still fall back to writing content; diagnostics may be omitted.
Notes
- Archive path detection runs before SQLite detection. A path that matches an archive selector is never treated as SQLite.
- SQLite detection declines when an existing file with a
.sqlite/.dbsuffix is present but does not have SQLite magic bytes; then the path falls back to a plain file write. - ZIP entry content is encoded with
new TextEncoder().encode(content)in#writeArchiveEntry(). Non-ZIP archive writes pass the string directly toBun.Archive.write(). - The prompt forbids two common anti-patterns: using
writefor routine edits that should useedit, and creating*.md/READMEfiles unless explicitly requested. It also forbids emojis unless requested. - Plain file and internal URL writes report
cleanContent.lengthas “bytes”, which is UTF-16 code units in JS, not an on-disk byte measurement. stripWriteContent()only removes hashline prefixes when the session’s file display mode hashashLinesenabled; otherwise content is written unchanged.