Extensions
September 20, 2026 · View on GitHub
Atomic can create extensions. Ask it to build one for your use case.
Extensions
Extensions are TypeScript modules that extend Atomic's behavior. They can subscribe to lifecycle events, register custom tools callable by the LLM, add commands, and more.
Placement for /reload: Put extensions in
~/.atomic/agent/extensions/(global) or.atomic/extensions/(project-local) for auto-discovery; legacy.pipaths remain supported. Useatomic -e ./path.tsonly for quick tests. Extensions in auto-discovered locations can be hot-reloaded with/reload.
Key capabilities:
- Custom tools - Register tools the LLM can call via
pi.registerTool() - Event interception - Block or modify tool calls, inject context, observe/cancel deletion-only compaction, and customize branch summaries
- User interaction - Prompt users via
ctx.ui(select, confirm, input, notify) - Custom UI components - Full TUI components with keyboard input via
ctx.ui.custom()for complex interactions - Custom commands - Register commands like
/mycommandviapi.registerCommand() - Session persistence - Store state that survives restarts via
pi.appendEntry() - Reload-surviving state - Keep in-memory objects alive across
/reloadviasessionScopedExtensionState() - Custom rendering - Control how tool calls/results and messages appear in TUI
Example use cases:
- Permission gates (confirm before
rm -rf,sudo, etc.) - Git checkpointing (stash at each turn, restore on branch)
- Path protection (block writes to
.env,node_modules/) - Compaction policies (cancel compaction or provide exact deletion targets)
- Conversation summaries (see
summarize.tsexample) - Interactive tools (questions, wizards, custom dialogs)
- Stateful tools (todo lists, connection pools)
- External integrations (file watchers, webhooks, CI triggers)
- Games while you wait (see
snake.tsexample)
See examples/extensions/ for working implementations.
Atomic also ships an environment-gated Herdr reporter. It combines settled agent activity, extension prompt events, and observed workflow roots under one parent pane owner. It defers to loaded community or legacy reporters and can be disabled with herdr.enabled in settings. See Herdr setup for the supported version and status indicators for reported activity.
Where to go next
Extensions are TypeScript modules that add tools, commands, event handlers, and custom UI. Read this page for startup behavior, locations, imports, and a first extension, then continue:
- Writing extensions — build one, manage its state, and register custom tools.
- Extension events — every event, its payload, and its return contract.
- Extension UI — render custom UI from an extension.
- Extension API reference —
ExtensionContext,ExtensionCommandContext,ExtensionAPImethods, and error handling. - Extension examples — runnable examples shipped with Atomic.
- Security — the project-trust boundary that decides whether a project's extensions load, and what an extension can reach once it does. Read this before installing an extension you did not write.
If an extension is heavier than you need, compare the lighter mechanisms on Build with Atomic.
Table of Contents
- Startup and lazy discovery
- Interactive callback isolation
- Quick Start
- Extension Locations
- Available Imports
- Writing an Extension
- Events
- Workflow activity and lifecycle hooks
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI Methods
- State Management
- Custom Tools
- Custom UI
- Error Handling
- Mode Behavior
- Examples Reference
Startup and lazy discovery
Built-in MCP, workflow, subagent, web-access, and Intercom commands are available at startup. Their first use may wait for discovery or connection.
Commands still wait for the resources they need before returning results. These include /workflow list, named workflow runs/inputs, failed or durable workflow resume, /mcp, direct MCP tool calls, mcp({ search }), mcp({ describe }), mcp({ server }), and explicit reload/setup flows.
Discovery scope depends on the operation:
- Cold-cache MCP proxy
describeloads metadata only for prefix-matched or explicitly requested servers. A prefix-directed miss does not start unrelated servers. - Cold-cache unscoped MCP proxy
searchloads metadata from all uncached lazy servers to search the full configured tool set. - Env-selected MCP direct tools warm only their selected servers and refresh live tool registration when ready.
- Paused live-workflow resume and pickers bypass full workflow discovery.
- Autocomplete falls back to current/admin completions when lazy discovery fails.
Failed first-use initialization can be retried. Cancelling one caller does not cancel initialization needed by others. Web-access batches with no successful items report a tool error; partial successes retain their completed items.
MCP tool timeoutMs is an inactivity limit, not a total deadline: progress resets it. Omit it to use the MCP SDK default.
Interactive callback isolation
Interactive sessions isolate extension callbacks from terminal input handling, so a busy callback does not stop keyboard handling or spinners.
Escape requests the engine's own cooperative cancellation and waits for it, for as long as the engine takes. There is no deadline on that wait, and Escape never terminates or replaces the engine, so an interrupt cannot discard in-flight tool state.
Ctrl+C is the host's escape hatch, and it applies in two distinct situations.
The first is a remote custom UI. While an engine-owned ctx.ui.custom() component or overlay holds input, every key is forwarded to the engine child, so a component that never resolves would trap Ctrl+C too. Ownership of the key is declared per mount:
- A component mounted with
handlesCtrlC: truereceives the press and keeps its own Skip, Close, or cancel binding. If that same component is still holding input on the next press, that press closes it, so a declared component cannot trap the keyboard either. - A component that did not declare it is closed by the first press, through the ordinary close path: its
ctx.ui.custom()promise resolves withundefined, the child is told the component closed, the editor comes back, and the engine keeps running — including any other component that generation has mounted below or above this one.
Declare handlesCtrlC whenever your component's hint row offers ctrl+c for anything. This is a migration for existing components: an extension that already bound Ctrl+C keeps that binding only by adding the option. The bundled workflow surfaces and the /mcp, /mcp setup, and MCP OAuth panels declare it. Native host selectors, dialogs, input forms, session pickers, and unrelated native overlays are unaffected: they keep Ctrl+C as their own cancel.
await ctx.ui.custom<string | undefined>(
(tui, theme, keybindings, done) => new PromptCard(tui, theme, done),
{ overlay: true, handlesCtrlC: true },
);
Both safety keys are matched by physical identity rather than by the configured app.clear action, so rebinding app.clear — even to Escape — can neither route Escape into a stop/restart branch nor take the host route away from Ctrl+C. A configured app.clear on any other key keeps its ordinary editor-clear behavior, and key-release events never act.
If the engine is unresponsive, an abort or replacement has waited over one second, or replacement failed, Ctrl+C terminates and replaces it. This takes precedence over custom UI bindings. A failed replacement remains recoverable with another Ctrl+C; Atomic does not keep retrying automatically.
After an unexpected engine stop, Atomic closes its custom UI and makes one automatic restart attempt. A failed restart leaves the editor usable and reports Interactive engine restart failed: …. A submission that never started returns as an exact draft, including pasted content and queued submissions in order. Accepted work is not offered for automatic retry.
If a saved tool call has no recorded result, reopening the session shows that its result is unavailable. Inspect files or external systems before retrying: the tool may already have had side effects.
When spawning processes from an extension, pass an explicit env derived from process.env. Engine-only bootstrap values are not exposed in that environment.
Dialogs and ctx.ui.custom() components are proxied to the host as rendered lines with asynchronous input forwarding. Custom UI results must be JSON-safe. APIs that require a synchronous callback in the terminal process—raw onTerminalInput transforms, synchronous getEditorText, custom editor factories, autocomplete wrappers, component-factory widgets, and custom header/footer factories—are unavailable in isolated interactive mode and produce a warning rather than executing extension code in the host. Print and public RPC modes retain their existing execution model.
Use ctx.ui.hostSessionPicker(request) for a session-style picker with responsive local navigation and search. Supply JSON-safe HostSessionPickerRow values: SessionInfo with createdAt and modifiedAt in epoch milliseconds.
The returned handle provides result, update(rows), error(message), and close(). result resolves to the selected path, or undefined on cancel. Your onDelete(path) callback owns deletion; the row remains until you call update or error. The API works in both interactive modes and is absent in print and headless RPC. See Host-native session picker.
For structured forms, use ctx.ui.hostInputForm(request). Supply JSON-safe descriptors of type string, text, number, integer, boolean, or select, each with a raw initialValue. It resolves to a raw string record or undefined on cancellation. Editing, navigation, validation, and configured keybindings work locally in both interactive modes. Print and headless RPC omit this optional API. See Host-native input form.
Quick Start
Create ~/.atomic/agent/extensions/my-extension.ts:
import type { ExtensionAPI } from "@bastani/atomic";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}
Test with --extension (or -e) flag:
atomic -e ./my-extension.ts
Extension Locations
Security: Extensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.
Extensions are auto-discovered from:
| Location | Scope |
|---|---|
~/.atomic/agent/extensions/*.ts | Global (all projects) |
~/.atomic/agent/extensions/*/index.ts | Global (subdirectory) |
.atomic/extensions/*.ts | Project-local |
.atomic/extensions/*/index.ts | Project-local (subdirectory) |
Atomic also discovers extensions and package resources inherited from legacy ~/.pi/agent and .pi configuration. When an inherited Pi extension uses the exact same tool, command, prompt, flag, or shortcut name as an extension bundled with Atomic, Atomic keeps the bundled registration and ignores only that conflicting inherited registration. Other resources from the inherited extension remain available. Interactive startup reports all such overlaps in one yellow summary; print and RPC modes apply the same winners without changing the Pi settings or package files.
This compatibility rule applies only to inherited Pi resources. Extensions configured through .atomic or passed explicitly with --extension retain the normal intentional override and load-order behavior described below.
Additional paths via settings.json:
{
"packages": [
"npm:@foo/bar@1.0.0",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}
To share extensions via npm or git as Atomic packages, see Atomic packages.
Available Imports
| Package | Purpose |
|---|---|
@bastani/atomic | Extension types (ExtensionAPI, ExtensionContext, events) |
typebox | Schema definitions for tool parameters |
@bastani/pi-ai | AI utilities (StringEnum for Google-compatible enums) |
@earendil-works/pi-tui | TUI components for custom rendering |
Registry dependencies work too. Add a package.json next to your extension (or in a parent directory), then install dependencies with Bun:
bun install
Imports from node_modules/ are resolved automatically.
For distributed Atomic packages installed with atomic install (npm or git), runtime deps must be in dependencies. Package installation uses production dependency installs by default, so devDependencies are not available at runtime; when npmCommand is configured, git packages use plain install for compatibility with wrappers.
Node.js built-ins (node:fs, node:path, etc.) are also available.
Writing an Extension
Moved to Writing extensions.
Async factory functions
Moved to Writing extensions.
Long-lived resources and shutdown
Moved to Writing extensions.
Extension Styles
Moved to Writing extensions.
Events
Moved to Extension events.
Lifecycle Overview
Moved to Extension events.
Startup Events
Moved to Extension events.
project_trust
Moved to Extension events.
Resource Events
Moved to Extension events.
resources_discover
Moved to Extension events.
Session Events
Moved to Extension events.
session_start
Moved to Extension events.
session_info_changed
Moved to Extension events.
session_before_switch
Moved to Extension events.
session_before_fork
Moved to Extension events.
session_before_compact / session_compact / session_compact_failed
Moved to Extension events.
session_before_tree / session_tree
Moved to Extension events.
session_shutdown
Moved to Extension events.
Agent Events
Moved to Extension events.
before_agent_start
Moved to Extension events.
agent_start / agent_end / agent_settled
Moved to Extension events.
ui_prompt_start / ui_prompt_end
Moved to Extension events.
turn_start / turn_end
Moved to Extension events.
message_start / message_update / message_end
Moved to Extension events.
tool_execution_start / tool_execution_update / tool_execution_end
Moved to Extension events.
context
Moved to Extension events.
before_provider_headers
Moved to Extension events.
before_provider_request
Moved to Extension events.
after_provider_response
Moved to Extension events.
Model Events
Moved to Extension events.
model_select
Moved to Extension events.
thinking_level_select
Moved to Extension events.
Tool Events
Moved to Extension events.
tool_call
Moved to Extension events.
Typing custom tool input
Moved to Extension events.
tool_result
Moved to Extension events.
User Bash Events
Moved to Extension events.
user_bash
Moved to Extension events.
Input Events
Moved to Extension events.
input
Moved to Extension events.
ExtensionContext
Moved to Extension API reference.
ctx.ui
Moved to Extension API reference.
ctx.hasUI
Moved to Extension API reference.
ctx.cwd
Moved to Extension API reference.
ctx.isProjectTrusted()
Moved to Extension API reference.
ctx.sessionManager
Moved to Extension API reference.
ctx.modelRegistry / ctx.model / ctx.scopedModels
Moved to Extension API reference.
ctx.signal
Moved to Extension API reference.
ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
Moved to Extension API reference.
ctx.isProjectTrusted()
Moved to Extension API reference.
ctx.shutdown()
Moved to Extension API reference.
ctx.getContextUsage()
Moved to Extension API reference.
ctx.compact()
Moved to Extension API reference.
ctx.getSystemPrompt()
Moved to Extension API reference.
ctx.getSkillCatalog()
Moved to Extension API reference.
ExtensionCommandContext
Moved to Extension API reference.
ctx.waitForIdle()
Moved to Extension API reference.
ctx.newSession(options?)
Moved to Extension API reference.
ctx.fork(entryId, options?)
Moved to Extension API reference.
ctx.navigateTree(targetId, options?)
Moved to Extension API reference.
ctx.switchSession(sessionPath, options?)
Moved to Extension API reference.
Session replacement lifecycle and footguns
Moved to Extension API reference.
ctx.reload()
Moved to Extension API reference.
ExtensionAPI Methods
Moved to Extension API reference.
pi.on(event, handler)
Moved to Extension API reference.
pi.registerTool(definition)
Moved to Extension API reference.
Built-in tool prompt contributions
Moved to Extension API reference.
pi.sendMessage(message, options?)
Moved to Extension API reference.
pi.sendMessages(messages, options?)
Moved to Extension API reference.
pi.sendUserMessage(content, options?)
Moved to Extension API reference.
pi.appendEntry(customType, data?)
Moved to Extension API reference.
pi.registerEntryRenderer(customType, renderer)
Moved to Extension API reference.
pi.setSessionName(name)
Moved to Extension API reference.
pi.getSessionName()
Moved to Extension API reference.
pi.setLabel(entryId, label)
Moved to Extension API reference.
pi.registerCommand(name, options)
Moved to Extension API reference.
pi.getCommands()
Moved to Extension API reference.
pi.registerMessageRenderer(customType, renderer)
Moved to Extension API reference.
pi.registerMarkdownTransformer(transformer)
Moved to Extension API reference.
pi.registerShortcut(shortcut, options)
Moved to Extension API reference.
pi.registerFlag(name, options)
Moved to Extension API reference.
pi.exec(command, args, options?)
Moved to Extension API reference.
pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Moved to Extension API reference.
pi.setModel(model)
Moved to Extension API reference.
pi.getThinkingLevel() / pi.setThinkingLevel(level)
Moved to Extension API reference.
pi.events
Moved to Extension API reference.
Native providers
Moved to Extension API reference.
pi.registerProvider(name, config)
Moved to Extension API reference.
pi.unregisterProvider(name)
Moved to Extension API reference.
State Management
Moved to Writing extensions.
Session-scoped in-memory state
Moved to Writing extensions.
Custom Tools
Moved to Writing extensions.
Tool Definition
Moved to Writing extensions.
Constrained sampling
Moved to Writing extensions.
Fireworks deferred tool loading
Moved to Writing extensions.
Overriding Built-in Tools
Moved to Writing extensions.
Remote Execution
Moved to Writing extensions.
Output Truncation
Moved to Writing extensions.
Multiple Tools
Moved to Writing extensions.
Custom Rendering
Moved to Writing extensions.
renderCall
Moved to Writing extensions.
renderResult
Moved to Writing extensions.
Keybinding Hints
Moved to Writing extensions.
Best Practices
Moved to Writing extensions.
Fallback
Moved to Writing extensions.
Custom UI
Moved to Extension UI.
Dialogs
Moved to Extension UI.
Timed Dialogs with Countdown
Moved to Extension UI.
Manual Dismissal with AbortSignal
Moved to Extension UI.
Widgets, Status, and Footer
Moved to Extension UI.
Autocomplete Providers
Moved to Extension UI.
Custom Components
Moved to Extension UI.
Overlay Mode (Experimental)
Moved to Extension UI.
Custom Editor
Moved to Extension UI.
Message Rendering
Moved to Extension UI.
Theme Colors
Moved to Extension UI.
Error Handling
Moved to Extension API reference.
Mode Behavior
| Mode | UI Methods | Notes |
|---|---|---|
| Interactive | Full TUI | Normal operation |
RPC (--mode rpc) | JSON protocol | Host handles UI, see RPC mode |
JSON (--mode json) | No-op | Event stream to stdout, see JSON mode |
Print (-p) | No-op | Extensions run but can't prompt |
In non-interactive modes, check ctx.hasUI before using UI methods.
Examples Reference
Moved to Extension examples.
Workflow activity and lifecycle hooks
Moved to Extension events.