Publishing Extensions
Last updated: Aug 7, 2026
$ pi –publish
TL;DR. Local-first: drop a file in ~/.pi/agent/extensions/ or .pi/extensions/, or run pi -e ./my-extension.ts for a quick test; /reload hot-loads. To distribute, ship a package.json with a pi: { extensions: [...] } field pointing at the compiled JS and every runtime dependency in dependencies (not devDependencies — Pi installs with --omit=dev). Test via npm link + pi install link:, then publish to npm or push to a git host and install via pi install git:.... Extensions run with your full system permissions — declare what you need in the README.
A Pi extension starts life as a single TypeScript file. It graduates to a distributable package when you want other people — or your future self across multiple machines — to install it with pi install. This guide covers the structure of an extension package, the pi field in package.json, how to test locally before publishing, and the gotchas around runtime dependencies. It follows the Extensions documentation, the Packages documentation, and the working examples under packages/coding-agent/examples/extensions.
The minimum viable extension
Before worrying about packaging, make sure your extension works as a plain file. The smallest useful shape:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(_id, params) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
}
Save it as greet.ts. Test it without installing anything:
pi -e ./greet.ts
Inside the TUI, the LLM can now call the greet tool with a name parameter. If it does, you will see the registered tool listed under the available tools.
Where extensions are discovered
For local development, Pi picks up .ts files from these locations automatically:
~/.pi/agent/extensions/*.ts— global, single file~/.pi/agent/extensions/*/index.ts— global, subdirectory.pi/extensions/*.ts— project-local, single file.pi/extensions/*/index.ts— project-local, subdirectory
Project-local extensions load only after the project has been trusted (resolved through the project_trust event). You can trigger a reload without restarting with /reload.
For non-standard paths — your own scratch directory, a checkout of someone’s repo — list them in settings.json:
{
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}
Promoting a file to a package
When an extension grows beyond a single file or needs third-party dependencies, package it. The minimal package.json looks like this:
{
"name": "pi-greet",
"version": "0.1.0",
"description": "Adds a greet tool to Pi.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"typebox": "^0.32.0",
"@earendil-works/pi-coding-agent": "^0.83.0"
},
"pi": {
"extensions": ["./dist/index.js"]
}
}
Two details that catch people out:
pi.extensionsis the entry-point manifest. It points at compiled JS (not the TS source) so Pi can load the package without a TypeScript toolchain at runtime.dependencies, notdevDependencies. Package installation uses production installs (npm install --omit=dev) by default, so anything youimportat runtime must be independencies.
A peerDependencies entry on @earendil-works/pi-coding-agent is optional but recommended, since Pi itself ships with the matching version. Pinning loosely (^0.83.0) avoids spurious version conflicts when users upgrade Pi independently.
Testing the package before publishing
Before pushing to npm, link the package locally so a pi install of the local path works exactly as it would for an end user:
# Inside your extension repo
npm link
# Globally available; now test inside any project
pi install link:pi-greet
# Or, point at the built directory directly
pi -e /absolute/path/to/pi-greet/dist/index.js
Once linked, exercise every entry point: register a tool, subscribe to a session event, render custom UI. Confirm hot reload with /reload and that the extension survives a full Pi restart.
Publishing to npm
Standard npm publishing works:
npm login
npm publish --access public
Make sure dist/ is up to date (compile your TypeScript with your preferred tool — tsc, tsup, unbuild), package.json lists every runtime dependency, and the files field (or .npmignore) keeps source maps and dev artifacts out of the tarball.
After publishing, install from a fresh shell:
pi install npm:pi-greet
pi
Inside the new Pi session, the greet tool should be available just as it was during local testing.
Publishing to a git host
Git-based packages are useful when you want to install a private fork or pin to a specific commit. The package still needs a package.json with a pi field; users install with:
pi install git:github.com/your-org/pi-greet@v1.0.0
The Packages documentation covers the supported URL shapes and how to point at a subdirectory in a monorepo. Note that git installs use plain npm install (not --omit=dev) when npmCommand is configured, so wrappers and monorepos with hoisted dependencies work without surprise.
What you can build on
A handful of official packages are exposed to extension authors:
| Package | Use it for |
|---|---|
@earendil-works/pi-coding-agent |
Extension types (ExtensionAPI, ExtensionContext, events) |
typebox |
JSON Schema definitions for tool parameters |
@earendil-works/pi-ai |
AI utilities (StringEnum for Google-compatible enums) |
@earendil-works/pi-tui |
TUI components for custom rendering |
If your package needs to add a custom provider rather than just a tool, see the Custom providers documentation. The full-provider extension surface (registration, model refresh, filtering, custom streaming) shipped in v0.81.0.
Permissions, trust, and disclosure
Pi extensions run with your full system permissions and can execute arbitrary code. The agent makes no sandboxing attempt, so the install trust model is “whoever ships the package ships the code.” Two practices keep your users safe and your package credible:
- State the permissions your extension needs in the README. If you touch the network, write files outside the working directory, or invoke subprocesses, say so.
- Keep the entry-point surface narrow. A single
pi.extensionsentry that re-exports everything is easier to audit than a sprawling tree of side-effect imports.
For project-local extensions, trust is gated by the project_trust event — extensions do not auto-load in a freshly cloned repo until the user has explicitly trusted it.
A small publishing checklist
Before tagging 1.0.0:
-
package.jsonhasname,version,description,type: "module",pi.extensions, and every runtime dependency. -
npm pack --dry-runlists only the files you intend. -
npm installin a clean directory produces a working package. -
pi install npm:your-packagein a fresh Pi session loads the extension. - README documents install, configure, and the permissions the extension uses.
- Version is pinned relative to
@earendil-works/pi-coding-agentto avoid breaking changes in the API.
For ecosystem visibility, drop a one-line entry on this site’s resource map under the extensions or tools category — see the Contributing page for the schema and submission flow.
Further reading
- Extensions — events, custom tools, slash commands, project trust
- Packages — install via npm or git,
pifield shape,--omit=devrules - Custom providers — when an extension is more than tools
packages/coding-agent/examples/extensions— working examples in the repo- Releases — full provider extensions in v0.81.0, blocked-tool-call termination in v0.84.1