Writer author documentation
Build a Writer extension
Package a compatible local tool for Writer, understand the extension contract, and verify it offline.
Writer supports installed JavaScript tool extensions across Pi, Codex account,
and Claude agent routes. Copy an extension package folder into
~/.writer/addons/extensions/, then quit and restart Writer. No
Writer rebuild is needed for compatible packages. Settings provides a shortcut
to this folder. A package is loaded when its agent process first starts;
restarting ensures all frames use the same installed version. A folder refresh
checks the manifest, not the execution of arbitrary code.
Only install code you trust. Extensions run as local code with your user account's filesystem and network access. They are not sandboxed. Installing an extension is an explicit choice to run it. Writer never downloads packages, runs an installer, or installs npm dependencies on your behalf. Remove its folder and restart to disable an extension. Do not modify Writer.app itself.
Create a package
Start with examples/extensions/text-statistics/: it runs entirely
offline. Copy the complete folder into the extensions folder and restart. Ask
the agent “Count the words in this document with text_statistics.” This
invokes a model; to check the tool without a model use the fixture tests
instead.
A package contains manifest.json and a JavaScript ES module:
{
"apiVersion": 1,
"id": "text-statistics",
"name": "Text statistics",
"description": "Count words in supplied text.",
"entry": "extension.js"
}
The entry must be a .js or .mjs file within the
package. Relative JavaScript imports and Node built-ins are available. Ship
other dependencies yourself; Writer does not promise access to its internal
npm dependencies. Export:
export default {
tools: [{
name: "text_statistics",
description: "Count words in supplied current text.",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
additionalProperties: false
},
async execute({ text }, { signal }) {
if (signal.aborted) throw new Error("Stopped");
return `Words: ${text.trim() ? text.trim().split(/\s+/u).length : 0}`;
}
}]
};
Return a string or
{content:[{type:"text",text:"..."}],isError?:boolean}. Images, UI widgets, and arbitrary Pi extension hooks are not part of this
Writer extension API. Tool results provide material to the agent; use
Writer's existing propose_edit tool for document changes so
the normal frame/document flow remains intact. Extensions receive supplied
arguments and an abort signal, not a private Writer API or credentials. A
web-fetch extension can use fetch(url, {signal}); avoid hidden
requests and preserve source attribution.
Do not write to stdout (console.log): agent processes use it for
RPC. Use stderr for non-sensitive development diagnostics. Never log
credentials or private document content. Keep module initialization fast and
side-effect free.
Compatibility and limits
-
Tool names: lowercase letter followed by lowercase letters, digits, or
_, at most 64 characters. Names must be unique across installed packages and cannot replace Writer's built-in tools. Packages load in folder-name order; a conflicting package is skipped as a whole. -
Portable JSON Schema supports
type,description,properties,required,additionalProperties(boolean),items,enum,minimum,maximum,minLength, andmaxLength; no$refor schema composition. Types are object, array, string, number, integer, boolean, and null. Root input must be object. Inputs are checked before execution on all routes. - Maximum 64 discovered package entries, 16 tools per package, 16 KiB manifest and each tool schema, 1 MiB entry file, 8 nested schema levels. Hidden folders are ignored. Manifest/entry paths cannot escape their package.
- Results are limited to 100,000 characters (strings are truncated), or 32 text content blocks within the same total size. Invalid results become tool errors.
- Cancellation and a 120-second timeout signal the tool to stop and end Writer's wait. Always pass the signal to asynchronous work. This is cooperative: Writer cannot undo side effects, terminate a blocked JavaScript event loop, or force trusted code that ignores the signal to stop.
- Invalid manifests, import failures, unsupported schemas, and duplicate names are isolated from valid packages. Sidecar conversations report safe load diagnostics; manifest discovery does not prove that the module can execute. Pi's loader skips invalid packages without breaking its built-in tools.
A new API version, native interface, or renderer capability still requires a Writer release. Updating compatible extension code only requires restarting.
Offline verification
From sidecar/, run bun test test/extensions.test.ts.
Tests create temporary packages, execute tools, reject invalid schemas and
escaped paths, isolate broken packages, and exercise cancellation without a
model or API key.