Pi extensions
runcell runs on the Pi coding agent engine. Pi extensions are host-side plugins that can register custom model providers, hook the agent lifecycle, transform requests, or add tools. pi.extensions is runcell's escape hatch into that system.
Security
Extensions execute in your host Node process with access to the filesystem, network, environment variables, and credentials. They are not sandboxed. Importing an extension is the trust decision; treat it like any other dependency that runs code.
Using a published extension
Extensions that export an SDK-compatible factory are ordinary npm packages. For example, pi-claude-auth authenticates Anthropic requests with your existing Claude Code credentials:
npm install pi-claude-authimport { createAgent } from 'runcell';
import claudeAuth from 'pi-claude-auth';
const agent = createAgent({
model: 'anthropic/claude-sonnet-4-5',
credentials: 'local',
pi: { extensions: [claudeAuth] },
});runcell supports the headless subset of Pi's extension API. Not every package on pi.dev will work: packages built for Pi's terminal UI (custom widgets, dialogs, keyboard shortcuts, themes, /commands) have no UI to attach to here, and packages that only declare extensions through a Pi manifest may not export an importable factory at all.
Writing an inline extension
runcell/pi exports the extension types and a defineExtension helper:
import { createAgent } from 'runcell';
import { defineExtension } from 'runcell/pi';
import { Type } from 'typebox';
const audit = defineExtension(pi => {
pi.on('tool_call', async event => {
console.log('tool:', event.toolName);
// return { block: true, reason: '...' } to veto the call
});
pi.registerTool({
name: 'record_finding',
label: 'Record Finding',
description: 'Record a single audit finding.',
parameters: Type.Object({ finding: Type.String() }),
execute: async (_id, params) => ({
content: [{ type: 'text', text: `recorded: ${params.finding}` }],
details: {},
}),
});
});
const agent = createAgent({
model: 'openai/gpt-5.1',
pi: { extensions: [audit] },
});Tools registered by supplied extensions are activated automatically. Importing the extension is the trust decision. For plain host functions you do not need an extension at all; the top-level tools option is simpler and typed.
Registering a custom provider works the same way and completes before the agent's model is resolved, so an extension can define the very provider the agent is configured to use:
const corp = defineExtension(pi => {
pi.registerProvider('corporate-ai', {
baseUrl: 'https://ai.corp.example/v1',
apiKey: '$CORPORATE_AI_KEY',
api: 'openai-completions',
models: [
/* ... */
],
});
});
createAgent({ model: 'corporate-ai/coder', pi: { extensions: [corp] } });Semantics
- Factories load in array order before the model is resolved. Later extensions may override providers registered by earlier ones, following Pi semantics.
- If a factory throws synchronously or asynchronously, the run rejects with
ExtensionErrorbefore any model request. Extensions should catch their own recoverable errors if needed, aspi-claude-authdoes. - If an extension tool shares a name with a Runcell tool or reserved sandbox tool (
read,write,edit,bash, …), the run rejects withExtensionError. Two extensions may shadow each other's tools; the last one wins, following Pi semantics. - Factories must be idempotent. Pi may rerun them under its reload semantics, and Runcell runs them for each run and thread resume. Use
session_startandsession_shutdownhandlers instead of starting background resources in the factory. session_shutdownis emitted and awaited during teardown, including errors and aborts.- In the headless environment,
ctx.hasUIisfalse. Dialogs, notifications, widgets, shortcuts, themes, and/commandsare unavailable. Becausesession_startmay not fire in embedded mode, preferagent_start. - Concurrent runs sharing an agent receive separate extension runtimes, but the factory closure and module state remain shared. Keep them safe for concurrent use.
- Extensions may override credentials configured by Runcell. Credential-writing extensions use Pi's local agent directory, so they work best with
credentials: 'local'or{ type: 'agentDir' }.
Versioning
The pi.extensions and runcell/pi APIs track Pi's extension API and versioning. They are outside Runcell's core stability guarantee.