Plugins live in .amp/plugins/ (project) or ~/.config/amp/plugins/ (global).
The Amp plugin API is experimental. Expect many breaking changes if you choose to use it right now. Do not use it for anything critical.
The plugin API supports:
amp.on(...) for hooking into tool calls, messages, and other eventsamp.registerTool(...) for custom toolsamp.registerCommand(...) to add to Amp's command palettectx.ui.notify(...), ctx.ui.confirm(...)amp.ai.ask(...) for yes-no LLM answersYou can use plugins to:
Run the Amp CLI with PLUGINS=all amp to use plugins. If the PLUGINS env var is not set, plugins are disabled. Plugins can execute arbitrary code, so only enable plugins when running Amp in trusted workspaces.
Limitations:
npm install.Use this prompt:
Make an Amp plugin to __________. See https://ampcode.com/manual/plugin-api for docs.
PLUGINS=all ampCtrl-o plugins: reload to reload all plugins after they're changed.// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now..amp/plugins/permissions.ts:
// @i-know-the-amp-plugin-api-is-wip-and-very-experimental-right-now
import type { PluginAPI } from '@ampcode/plugin'
export default function (amp: PluginAPI) {
// Ask the user before executing any tool.
amp.on('tool.call', async (event, ctx) => {
const confirmed = await ctx.ui.confirm({
title: `Allow ${event.tool}?`,
message: `The agent wants to execute the "${event.tool}" tool.`,
confirmButtonText: 'Allow',
})
if (!confirmed) {
ctx.logger.log(`User rejected tool execution: ${event.tool}`)
return {
action: 'reject-and-continue',
message: `User rejected execution of tool ${event.tool}.`,
}
}
return { action: 'allow' }
})
}
Amp's plugin API is inspired by pi's extension API, created by the awesome genius Mario Zechner.
@ampcode/plugin Package Types/**
* # Amp Plugin API
*
* Plugins are JavaScript/TypeScript programs that extend & customize Amp.
* They are long-lived processes that may run for multiple threads concurrently.
*
* Plugins live in `.amp/plugins/` (project) or `~/.config/amp/plugins/` (global) and are executed using Bun.
*
* A plugin exports a default function that receives a {@link PluginAPI} instance. For example:
*
* ```ts
* import type { PluginAPI } from '@ampcode/plugin'
*
* export default function (amp: PluginAPI) {
* amp.on('session.start', (event, ctx) => {
* ctx.ui.notify('Welcome')
* })
* }
* ```
*/
/**
* The plugin API object passed to the plugin's default export function.
*/
export interface PluginAPI {
/** Logger scoped to this plugin */
logger: PluginLogger
/** System capabilities and environment information */
system: PluginSystem
/** Observable configuration that streams changes */
configuration: PluginConfiguration<Record<string, unknown>>
/**
* Execute shell commands using Bun's shell.
* Unlike `ctx.$` in event handlers, this is not tied to a specific hook invocation.
*/
$: ShellFunction
/**
* Helper utilities for interpreting tool events.
*/
helpers: {
shellCommandFromToolCall: ShellCommandFromToolCall
toolCallsInMessages: ToolCallsInMessages
filesModifiedByToolCall: FilesModifiedByToolCall
}
/**
* Register a handler for plugin events.
* For request events (e.g., tool.call), the handler must return a result.
* For fire-and-forget events, the handler returns void.
*
* If multiple plugins listen on the same event, the order in which each plugin's event handler is executed is not defined.
*/
on<E extends keyof PluginEventMap>(
event: E,
handler: (event: PluginEventMap[E], ctx: PluginEventContext) => PluginHandlerResult<E>,
): Subscription
/**
* Register a command that appears in Amp's command palette.
* When the user invokes the command, the handler is called.
*
* @param id - Stable identifier for the command (e.g., "hello-world").
* @param options - Configuration for the command including title, category, and description.
* @param handler - The function to execute when the command is invoked.
*
* @example
* ```ts
* amp.registerCommand('hello-world', { title: 'greet', category: 'hello', description: 'Say hello' }, async (ctx) => {
* await ctx.ui.notify('Hello, world!')
* })
* ```
*/
registerCommand(
id: string,
options: PluginCommandOptions,
handler: (ctx: PluginCommandContext) => void | Promise<void>,
): Subscription
/**
* Register a tool that the agent can call.
* Plugin tools appear alongside built-in tools and can be invoked by the LLM during conversations.
*
* @param definition - The tool definition including name, description, schema, and execute handler.
*
* @example
* ```ts
* amp.registerTool({
* name: 'hello',
* description: 'Greet someone by name',
* inputSchema: {
* type: 'object',
* properties: { name: { type: 'string', description: 'Name to greet' } },
* required: ['name'],
* },
* async execute(input) {
* return `Hello, ${input.name}!`
* },
* })
* ```
*/
registerTool(definition: PluginToolDefinition): Subscription
/** AI helpers */
ai: PluginAI
}
/**
* Result from an AI ask operation.
*/
export interface PluginAIAskResult {
/** The classification result: 'yes', 'no', or 'uncertain' */
result: 'yes' | 'no' | 'uncertain'
/** Probability (0-1) that the answer is yes */
probability: number
/** Explanation of why the AI gave this answer */
reason: string
}
/**
* AI capabilities provided to plugins.
*/
export interface PluginAI {
/**
* Ask an AI model a yes/no question and get a confidence-based response with reasoning.
* @param question - The yes/no question to ask
* @returns Object with result, probability, and reason
*/
ask(question: string): Promise<PluginAIAskResult>
}
/**
* Observer interface for subscribing to configuration changes.
*/
export interface PluginConfigurationObserver<T> {
next?(value: T): void
error?(error: unknown): void
complete?(): void
}
/**
* Subscription that can be unsubscribed to release resources.
*/
export interface Subscription {
unsubscribe(): void
}
/**
* Target for configuration updates.
*/
export type PluginConfigurationTarget = 'workspace' | 'global'
/**
* Observable-like interface for Amp configuration.
* Provides a limited subset of Observable functionality for plugins.
*/
export interface PluginConfiguration<T> {
/**
* Subscribe to configuration changes.
*/
subscribe(observer: PluginConfigurationObserver<T>): Subscription
subscribe(onNext: (value: T) => void): Subscription
/**
* Pipe operators for transforming the configuration observable.
*/
pipe<Out>(op: (input: PluginConfiguration<T>) => Out): Out
/**
* Get the current configuration value.
*/
get(): Promise<T>
/**
* Update configuration with partial values.
* @param partial - The partial configuration to merge
* @param target - Where to store the setting: 'global' (user settings) or 'workspace' (default)
*/
update(partial: Partial<T>, target?: PluginConfigurationTarget): Promise<void>
/**
* Delete a configuration key.
* @param key - The key to delete
* @param target - Where to delete from: 'global' (user settings) or 'workspace' (default)
*/
delete(key: keyof T, target?: PluginConfigurationTarget): Promise<void>
}
/**
* Logger provided to plugins for scoped logging.
*/
export interface PluginLogger {
log: (...args: unknown[]) => void
}
/**
* Bun shell function type (simplified version of Bun.$)
*/
export type ShellFunction = (
strings: TemplateStringsArray,
...values: unknown[]
) => Promise<ShellResult>
/**
* Result from a shell command execution.
*/
export interface ShellResult {
exitCode: number
stdout: string
stderr: string
}
/**
* Where plugin code is running relative to the interactive UI.
*/
export type PluginExecutorKind = 'local' | 'remote' | 'unknown'
/**
* Information about the executor running plugin code.
*/
export interface PluginExecutor {
readonly kind: PluginExecutorKind
}
/**
* System capabilities provided to plugins.
*/
export interface PluginSystem {
/**
* Open a URL using the system's default protocol handler.
* On the CLI, it also shows a dialog with the URL text (for SSH users who can't open URLs remotely).
*/
open(url: string | URL): Promise<void>
/**
* Get the effective Amp base URL currently used by this Amp client.
* This reflects the active runtime configuration (for example, custom domains via `AMP_URL`).
*/
readonly ampURL: URL
/**
* Information about the executor that is running this plugin.
*/
readonly executor: PluginExecutor
}
/** @internal */
export type SpanID = string & { readonly __brand: 'SpanID' }
export type ThreadID = `T-${string}`
/**
* A text content block in a message.
*/
export interface ThreadTextBlock {
type: 'text'
text: string
}
/**
* A thinking content block in a message.
*/
export interface ThreadThinkingBlock {
type: 'thinking'
thinking: string
}
/**
* A tool use content block in a message.
*/
export interface ThreadToolUseBlock {
type: 'tool_use'
id: string
name: string
input: Record<string, unknown>
}
/**
* A tool result content block in a message.
*/
export interface ThreadToolResultBlock {
type: 'tool_result'
toolUseID: string
output?: string
status: 'done' | 'error' | 'cancelled' | 'running' | 'pending'
}
/**
* A user message in the thread.
*/
export interface ThreadUserMessage {
role: 'user'
/** The message ID, which is unique in the thread. */
id: number
content: (ThreadTextBlock | ThreadToolResultBlock)[]
}
/**
* An assistant message in the thread.
*/
export interface ThreadAssistantMessage {
role: 'assistant'
/** The message ID, which is unique in the thread. */
id: number
content: (ThreadTextBlock | ThreadThinkingBlock | ThreadToolUseBlock)[]
}
/**
* An info message in the thread.
*/
export interface ThreadInfoMessage {
role: 'info'
/** The message ID, which is unique in the thread. */
id: number
content: ThreadTextBlock[]
}
/**
* A message in the thread (simplified view for plugins).
*/
export type ThreadMessage = ThreadUserMessage | ThreadAssistantMessage | ThreadInfoMessage
/**
* Thread API for manipulating the current thread.
*/
export interface PluginThread {
/**
* Append a user message to the thread.
*/
append(messages: UserMessage[]): Promise<void>
}
/**
* A user message that can be appended to the thread.
*/
export interface UserMessage {
type: 'user-message'
content: string
}
/**
* Options for the input dialog.
*/
export interface PluginInputOptions {
/** Dialog title */
title?: string
/** Help text/description shown below the title */
helpText?: string
/** Initial text value in the input field */
initialValue?: string
/** Text for the submit button (default: "Submit") */
submitButtonText?: string
}
/**
* Options for the confirm dialog.
*/
export interface PluginConfirmOptions {
/** Dialog title */
title: string
/** Message body shown below the title */
message?: string
/** Text for the confirm button (default: "Yes") */
confirmButtonText?: string
}
/**
* UI capabilities provided to plugins.
*/
export interface PluginUI {
notify(message: string): Promise<void>
/**
* Show an input dialog prompting the user for text input.
* @returns The entered text, or undefined if the user cancelled.
*/
input(options: PluginInputOptions): Promise<string | undefined>
/**
* Show a confirmation dialog with Yes/No options.
* @returns true if the user confirmed, false if they cancelled.
*/
confirm(options: PluginConfirmOptions): Promise<boolean>
}
/**
* URI value returned by helper APIs.
*
* This stays intentionally minimal so external plugin authors don't need
* Amp's internal URI package in their dependency graph.
*/
export interface URI {
toString(): string
}
/**
* Event payload for session.start event.
*/
export type SessionStartEvent = Record<string, never>
/**
* Event payload for tool.call event.
* This is a request that expects a response from the handler.
*/
export interface ToolCallEvent {
/** Unique identifier for this tool use (e.g., "toolu_xxx") */
toolUseID: string
/** Name of the tool that will be executed */
tool: string
/** Input arguments that will be passed to the tool */
input: Record<string, unknown>
}
/**
* Result returned from a tool.call handler.
* Determines how the tool execution should proceed.
*/
export type ToolCallResult =
/** Allow the tool to execute with its original input */
| { action: 'allow' }
/** Reject the tool call but allow the agent to continue with other tools */
| { action: 'reject-and-continue'; message: string }
/** Modify the tool's input arguments before execution */
| { action: 'modify'; input: Record<string, unknown> }
/** Provide a synthesized result without actually running the tool */
| { action: 'synthesize'; result: { output: string; exitCode?: number } }
/** Error occurred in the plugin - stops the thread worker and shows an ephemeral error */
| { action: 'error'; message: string }
/**
* Event payload for tool.result event.
*/
export interface ToolResultEvent {
/** Unique identifier for this tool use (e.g., "toolu_xxx") */
toolUseID: string
/** Name of the tool that was executed */
tool: string
/** Input arguments passed to the tool */
input: Record<string, unknown>
/** Result status of the tool execution */
status: 'done' | 'error' | 'cancelled'
/** Error message if status is 'error' */
error?: string
/** Tool output/result if available */
output?: unknown
}
/**
* Result returned from a tool.result handler.
* Allows modifying the tool result before it is sent back to the model.
*/
export type ToolResultResult =
| {
status: 'done'
output?: unknown
}
| {
status: 'error'
error?: string
output?: unknown
}
| {
status: 'cancelled'
error?: string
output?: unknown
}
| undefined
| void
/**
* Event payload for agent.start event.
* Fired when a user submits a prompt (initial or reply).
*/
export interface AgentStartEvent {
/** The user's prompt message */
message: string
/** The message ID */
id: number
}
/**
* Result returned from an agent.start handler.
* Allows adding context messages or modifying the system prompt.
*/
export interface AgentStartResult {
/**
* A message to append after the user's content in the user message.
* If display is true, the message is shown in the UI.
*/
message?: { content: string; display: true }
}
/**
* Event payload for agent.end event.
* Fired when the agent finishes handling a user prompt.
*/
export interface AgentEndEvent {
/** The user's prompt message that started this turn */
message: string
/** The message ID that started this turn */
id: number
/** The outcome of the agent's turn */
status: 'done' | 'error' | 'interrupted'
/** All messages since the agent.start event (including the user message that started this turn) */
messages: ThreadMessage[]
}
/**
* Result returned from an agent.end handler.
* Allows starting a new agent turn by returning a user message.
*/
export type AgentEndResult =
/** Automatically send a follow-up user message to start a new agent turn */
{ action: 'continue'; userMessage: string } | void
/**
* Map of event names to their payload types.
*/
export interface PluginEventMap {
'session.start': SessionStartEvent
'tool.call': ToolCallEvent
'tool.result': ToolResultEvent
'agent.start': AgentStartEvent
'agent.end': AgentEndEvent
}
/**
* Map of request event names to their result types.
* These events expect a response from the handler.
*/
export interface PluginRequestResultMap {
'tool.call': ToolCallResult
'tool.result': ToolResultResult
'agent.start': AgentStartResult
'agent.end': AgentEndResult
}
/**
* Context passed as the second argument to event handlers.
*/
export interface PluginEventContext {
/** Scoped logger for plugin output. Log messages are appended to the handler's trace span events. */
logger: PluginLogger
/** Bun's shell API for executing commands */
$: ShellFunction
/** Platform UI capabilities */
ui: PluginUI
/** AI capabilities */
ai: PluginAI
/** System capabilities */
system: PluginSystem
/** Thread manipulation API */
thread: PluginThread
/** The trace span ID for this handler invocation, if tracing is enabled */
span?: SpanID
}
/**
* Handler return type based on whether the event expects a response.
* Request events (in PluginRequestResultMap) must return a result.
* Fire-and-forget events return void.
*/
export type PluginHandlerResult<E extends keyof PluginEventMap> =
E extends keyof PluginRequestResultMap
? PluginRequestResultMap[E] | Promise<PluginRequestResultMap[E]>
: void | Promise<void>
/**
* Standardized shell command representation.
*/
export interface ShellCommand {
command: string
dir?: string
}
/**
* A tool call and its corresponding terminal tool result extracted from thread messages.
*/
export interface ToolCallWithResult {
call: ToolCallEvent
result: ToolResultEvent
}
/**
* Extracts the shell command from a Bash or shell_command tool call.
* Returns null if the event is not a shell command tool call.
*/
export type ShellCommandFromToolCall = (event: ToolCallEvent) => ShellCommand | null
/**
* Extracts paired tool calls and terminal tool results from a list of thread messages.
*/
export type ToolCallsInMessages = (messages: ThreadMessage[]) => ToolCallWithResult[]
/**
* Returns an array of file URIs modified by a tool call, or null if the tool doesn't modify files.
* Supports edit/create/apply_patch tools and sed in-place shell commands.
*/
export type FilesModifiedByToolCall = (event: ToolCallEvent | ToolResultEvent) => URI[] | null
/**
* Options for registering a command.
*/
export interface PluginCommandOptions {
/** The title shown after the colon in the command palette (e.g., "Greet" in "Hello: Greet") */
title: string
/** The category shown before the colon (e.g., "Hello" in "Hello: Greet"). Defaults to the plugin name. */
category?: string
/** Human-readable description of what this command does */
description?: string
}
/**
* Context passed to command handlers.
* Provides access to UI capabilities for executing command actions.
*/
export interface PluginCommandContext {
/** Platform UI capabilities */
ui: PluginUI
/** AI capabilities */
ai: PluginAI
/** System capabilities */
system: PluginSystem
/** Bun's shell API for executing commands */
$: ShellFunction
/** Current thread context if a thread is active, undefined otherwise */
thread?: {
id: ThreadID
}
}
/**
* Context passed to tool execute handlers.
*/
export interface PluginToolContext {
/** Scoped logger for plugin output */
logger: PluginLogger
}
/**
* Options for registering a tool that the agent can call.
*/
export interface PluginToolDefinition {
/** Tool name (must match ^[a-zA-Z0-9_-]+$) */
name: string
/** Description shown to the LLM explaining what the tool does */
description: string
/** JSON Schema for the tool's input parameters */
inputSchema: {
type: 'object'
properties?: Record<string, object>
required?: string[]
[key: string]: unknown
}
/** Execute the tool with the given input and return a result */
execute: (input: Record<string, unknown>, ctx: PluginToolContext) => Promise<unknown>
}