Skip to main content
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 .pi paths remain supported. Use atomic -e ./path.ts only for quick tests. Extensions in auto-discovered locations can be hot-reloaded with /reload.
If /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 /mycommand via pi.registerCommand()
  • Session persistence - Store state that survives restarts via pi.appendEntry()
  • Reload-surviving state - Keep in-memory objects alive across /reload via sessionScopedExtensionState()
  • 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.ts example)
  • 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.ts example)
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, ExtensionAPI methods, 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

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() and ALL_TOOLS require all enabled servers. Tool search and resource tools also wait for discovery.
  • /mcp provides server management; /mcp login, /mcp logout, and /mcp reconnect accept 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.
Failed connections can be retried with /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-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: true receives 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 with undefined, 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 declare it. Native host selectors, dialogs, input forms, session pickers, and unrelated native overlays are unaffected: they keep Ctrl+C as their own cancel.
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:
Test with --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 exposes builtin:llama.cpp, builtin:codemode, and builtin:tool-search as extension resources, not filesystem paths. Disable one in the extensions setting, for example:
A trusted project "+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:
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_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 with prepareLoadout(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 or session_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’s renderCall 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. 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.

Examples Reference

Moved to Extension examples.

Workflow activity and lifecycle hooks

Moved to Extension events.