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 inIf~/.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.
/reload reports Reload retiring cleanup failed, its error includes the extension cleanup cause. The replacement extensions and updated settings remain active, and running workflows and joined Intercom groups stay available. Fix the reported extension cleanup error before reloading again.
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
- 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)
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.
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 tool calls, and explicit reload flows.
Discovery scope depends on the operation:
- Enabled MCP servers connect in the background when a session starts. The first prompt waits up to 10 seconds for servers with direct tools.
- Codemode waits for the MCP namespaces a script names;
searchTools()andALL_TOOLSrequire all enabled servers. Tool search and resource tools also wait for discovery. /mcpprovides server management;/mcp login,/mcp logout, and/mcp reconnectaccept an optional server name. Completion lists actions and eligible servers.- Paused live-workflow resume and pickers bypass full workflow discovery.
- Autocomplete falls back to current/admin completions when lazy discovery fails.
/mcp reconnect; dropped MCP connections reconnect on the next call. Web-access batches with no successful items report a tool error; partial successes retain their completed items.
MCP timeout is an inactivity limit in seconds, not a total deadline: progress resets it. The default is 60 seconds. See MCP servers for native configuration, exposure, resources, OAuth, and shell commands.
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-ownedctx.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.
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 declare it. Native host selectors, dialogs, input forms, session pickers, and unrelated native overlays are unaffected: they keep Ctrl+C as their own cancel.
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:
--extension (or -e) flag:
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:
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:
Built-in extension resources
Atomic exposesbuiltin:llama.cpp, builtin:codemode, and builtin:tool-search as extension resources, not filesystem paths. Disable one in the extensions setting, for example:
"+builtin:codemode" entry can override a global exclusion. Explicit loading also works with --no-extensions: atomic --no-extensions -e builtin:codemode --tools read,codemode. Loading the resource registers the tool; activating it still requires defaultTools or --tools because codemode and tool search are inactive by default.
Codemode and tool search are replaceable: an extension that registers the same tool, command, or flag can take over. Atomic reports the replacement and names its owner. Disable that extension through atomic config when you want the built-in implementation instead.
These symbolic names do not replace Atomic’s bundled MCP, workflow, subagent, web-access, or Intercom resources. There are no corresponding builtin:mcp, builtin:workflows, or builtin:subagents aliases. Keep using the existing package controls and MCP configuration.
To share extensions via npm or git as Atomic packages, see Atomic packages.
Available Imports
Registry dependencies work too. Add a
package.json next to your extension (or in a parent directory), then install dependencies with Bun:
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_before_settle / 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.context_with_system
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(sessionId, 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. A tool that orchestrates other tools can adjust what the model sees withprepareLoadout(loadout). The hook receives declared, callable, and registered tools, with getExposure(), getNamespace(), and getPromptGuidelines(). Return replacement descriptions or hiddenDeclarations to hide active tools’ declarations while keeping them callable. The default system prompt omits hidden tools from its tool list and rules, and names no tool in the skills hint when the file reader is hidden. The orchestrating tool should present those guidelines itself, as codemode does.
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.pi.registerMcpServer(name, config)
Contribute an MCP server computed at runtime from the factory orsession_start, using the native mcpServers entry shape. Registered servers connect like file-configured servers and appear in /mcp with the extension as their source. File-configured servers take precedence; enabled-state and exposure changes to registered servers apply only to the session. Shell atomic mcp commands do not load extensions. Provider-token authentication is allowed for extension registrations and global configuration, but not project configuration or package manifests. See MCP servers and 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.Tool rendering
A tool’srenderCall and renderResult draw its calls in the interactive transcript and HTML exports. Use pi.registerToolRenderer((toolName, next) => renderers) to choose renderers for any tool, including MCP tools in resumed sessions before their server connects. Resolvers run in extension load order. next() returns the remaining resolvers’ choice, then the registered tool’s renderers, so next() ?? mine only fills in when none exist.
Extension Image components render JPEG, GIF, and WebP images on Kitty-protocol terminals automatically, just like tool result images.
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
In non-interactive modes, check
ctx.hasUI before using UI methods.