Semantic event
Unresolved signal
The capability is not available. The observer rests outside the architecture and does not originate the unresolved state.
Observer responseResting offset state
The observer is an original decorative stand-in, not Puck, Amp, a plugin, or an action source. Puck artwork rights and attribution remain unresolved.
The observer above is an original decorative stand-in, not official Puck art. It echoes semantic events only after the architecture changes, and it retires once the first process model stabilizes.
If you have never thought about that machinery, it helps to follow one plugin from the moment its capability is absent to the moment Amp can use it. An ordinary plugin, in this article, is code that contributes capabilities such as commands, tools, skills, events, modes, or interface actions. The plugin is not itself any one of those things. It has to start, establish communication with Amp, and report what it provides before the capability can appear.
The lifecycle is small enough to hold in your head. It also corrects a tempting first picture: Amp does not simply absorb a plugin's code and continue as though the code had always been part of the application. The two sides establish a working relationship, one explicit step at a time.
01 | What are we following?
A capability that is not there yet
Imagine opening Amp before the plugin has loaded. The capability we care about does not exist from Amp's point of view. The plugin's files may be present, and Amp may have decided to load them, but a file on disk is not yet a working command or tool. There is still a lifecycle to complete.
This distinction is useful throughout the article. The plugin is the running participant. Commands, tools, skills, event handlers, and modes are things it can register. An interface action is something it can ask Amp to perform. None of those should be treated as proof that the plugin is already active.
Model 01 | Before startup
Capability unavailable.
Local plugin code and a live Amp capability are different states.
Causal checkpoint
The connector stops. Code on disk does not make the capability available.
Files are not availability
A plugin module may exist locally while the dashed target slot remains unavailable to Amp. Limited static metadata discovery does not replace the live registration and readiness protocol.
Starting from absence gives us a simple test for every step that follows: has Amp learned enough to make the capability available yet? At this point, the answer is no. Before Amp can learn anything from the plugin, it needs somewhere to run it.
The official Amp Plugins guide describes the capability surfaces plugins can provide. The live process and readiness sequence shown here comes from the versioned bundle analysis described near the end.
02 | Where does it run?
A process of its own
For each ordinary plugin, Amp starts an isolated Bun child process. Here, isolated means the plugin runs in a separate operating-system process from the main Amp process. It is not a claim that the process is a security sandbox; process separation alone does not establish those properties.
The main Amp process coordinates the session and presents its interface. The plugin process is the child in which this one plugin runs. There is one such child for each ordinary plugin, placing each plugin on the other side of a process boundary from the main application.
Amp creates that runtime by re-executing its own binary with BUN_BE_BUN=1. To re-execute a binary is to start that program again as a new process. In this mode, the child behaves as a Bun runtime for the plugin code. This does not produce a second visible Amp application, and the child is not a separate plugin server. It is a local process with a specific job.
Model 02 | Process ownership
One plugin, one child process.
The ordinary plugin runs in a separate local Bun process, outside the Amp main process.
Causal checkpoint
The boundary is an operating-system process boundary. It is not a security sandbox or network server.
Separate process, separate responsibility
Amp re-executes its binary with BUN_BE_BUN=1. The result is one local Bun-mode child for this ordinary plugin, not another visible Amp application.
We now have a more accurate picture than "Amp loads some code." There are two processes with different responsibilities. That creates an immediate constraint: they cannot rely on ordinary in-process function calls. They need a channel across the boundary.
03 | How do they communicate?
Two streams and a small protocol
Amp communicates with the child through its standard input and standard output, usually shortened to stdin and stdout. These streams carry messages between the main process and plugin runtime.
The messages use newline-delimited JSON RPC. JSON gives each message a structured shape. Newline-delimited means one complete message ends at a newline. RPC, or remote procedure call, means one side can ask the other to perform an operation and receive a result. "Remote" describes a call crossing the local process boundary here; it does not imply a network server.
Think of the stream as a roll of paper where each line contains one complete form. The newline tells the reader where one form ends. The real runtime exchanges JSON rather than paper, but the analogy explains how both sides recognize messages on a continuous stream.
Model 03 | Explicit transport
Two streams cross the boundary.
Newline-delimited JSON RPC messages travel through stdin and stdout in both directions.
Causal checkpoint
Messages cross explicit lanes. The example JSON is illustrative, not a verified Amp method or schema.
A local protocol, not a network hop
One newline ends one JSON message. Requests, responses, and runtime events can cross the same two process streams without shared memory.
The important change is not the punctuation. Communication is explicit. Requests leave one process, cross the boundary, and are handled by the other. Responses and runtime events use the same deliberate channel.
04 | What runs first?
The startup handshake
The child begins by requesting client.info. The plugin runtime asks for information about the Amp client it is joining. The response contents are outside this article's scope, but the order matters: the child asks before it imports and runs the plugin.
After receiving context, the child imports the plugin module. Import makes the module available to the runtime, but it does not make the plugin ready. The child then invokes the plugin's default export with amp.
The amp value is the interface the plugin receives for interacting with Amp. It gives the plugin a defined way to participate in the relationship without reaching into the main process directly.
Model 04 | Ordered startup
Import is not readiness.
The child asks for context, imports the module, invokes the entry function, reports changes, and then sends runtime.ready.
Causal checkpoint
The order stays visible. client.info, import, invocation, changes, and runtime.ready are separate steps.
The startup order
- The child requests
client.info. - Amp returns context.
- The child imports the module.
- It invokes
default export(amp). - Registration changes follow.
runtime.readyfollows the changes.
Compressing this to "Amp runs the plugin" would hide a useful boundary. A successful import says only that the module was imported. Even after invocation, Amp has not necessarily collected a finished set of capabilities.
05 | How does Amp learn what changed?
Registration becomes readiness
As the plugin runs, it can register capabilities. A registration is the plugin declaring something it contributes: a command, tool, skill, event, or mode. The child emits changes as registrations are made, but a stream of changes does not mean startup is complete.
The distinct completion signal is runtime.ready. It marks the end of the runtime's startup sequence. It does not carry every command and tool inside one ready package. After receiving readiness, Amp fetches commands, tools, skills, events, and modes from the child.
Keep five states separate: the plugin code exists; its process runs and invokes the entry function; the plugin registers capabilities; the runtime declares itself ready; then Amp fetches those registrations and makes the capabilities available. Each state depends on the one before it, but none can stand in for the next.
Model 05 | Registration and readiness
Registration becomes readiness.
Changes, runtime.ready, fetches, and availability are four different causes.
Causal checkpoint
runtime.ready carries no capability tokens. Amp fetches five ordered groups after readiness, then makes the capability available.
- Changes. Registrations accumulate in the child.
- Ready. The gate opens after
runtime.ready. - Fetch. Amp requests commands, tools, skills, events, and modes.
- Available. Results enter Amp's rack.
This is why module import cannot stand in for readiness. A module may import before its entry function finishes. Registrations may change while startup is underway. runtime.ready gives the relationship an explicit boundary, and Amp's subsequent fetch turns pending registrations into capabilities the main process knows about.
The official execute-mode documentation describes plugin loading and its readiness timeout at the product boundary. The exact changes, ready, fetch, and availability order shown here was verified from the normalized CLI bundle.
06 | Who renders plugin UI?
When the plugin asks Amp to draw
A plugin running in a child process does not render the Amp interface inside that child. Its UI APIs work as reverse RPC calls to the main Amp process. Reverse RPC is not a different transport; it names the direction of the request.
Suppose the plugin needs Amp to show an interface element. The plugin requests that action through its API. The request travels from child to main process, and Amp renders the interface. The plugin owns the request; Amp owns the presentation.
Model 06 | Reverse request direction
The plugin asks; Amp draws.
A UI request travels toward Amp over the existing transport before a panel appears in the main process.
Causal checkpoint
The panel appears only inside Amp. Reverse RPC names request direction, not another transport.
Ownership stays split
The plugin owns the request. Amp handles it and owns the presentation. This model does not require an intermediary or an unverified rendering framework.
The child is not a tiny copy of the application with a private interface waiting to be revealed. It is a runtime that can ask the main process to perform supported UI actions. We do not need to name Amp's rendering framework to understand that division of responsibility.
The official Plugin API reference documents the supported UI-facing API surface. The reverse request direction and process ownership shown here were verified from the versioned CLI bundle.
07 | Which failures retry?
Ready is a real boundary
A plugin that fails before runtime.ready has not completed the startup contract. Amp may have started the child, answered client.info, or reached module import, but the relationship never crossed readiness. The capability never reaches the same available state as a successful load.
That differs from an active plugin crashing after it became ready. In the active-crash case, Amp can make three automatic restarts after the initial run, for up to four launches total. Restarts use backoff, meaning they are separated rather than made in an uncontrolled loop. No exact delay should be inferred.
Model 07 | Failure boundary
Ready selects the failure path.
Before-ready failure stops startup. An active crash withdraws capability and can enter a bounded restart path.
Causal checkpoint
Only the active-crash branch retries. Three restarts follow the initial launch, allowing up to four launches total.
Before ready
The capability never becomes available and this path has no automatic restart continuation.
After active
Amp withdraws the capability, then may perform restart 1, restart 2, and restart 3. Success is not guaranteed.
"The plugin failed" is too broad to explain the lifecycle. A load failure means readiness never happened. An active crash interrupts a relationship that already produced capabilities, so Amp withdraws them through its own registry cleanup and may enter the bounded retry path.
08 | How does it stop?
Ending the relationship cleanly
Dispose names a deliberate closing phase. Terminate means ending the child process. They are related, but they are not synonyms.
Amp first withdraws the plugin's capabilities from its active registries. If the child is still running, reached readiness, and is not terminally failed, Amp asks it to dispose. The child runs registered callbacks and replies. Callback failures are logged and ignored, so cleanup remains best-effort.
Amp waits for the reply, child exit, or a three-second timeout. If the child remains alive, Amp sends SIGINT, waits up to another three seconds, and uses SIGKILL only if it is still alive. An unexpected crash does not run dispose callbacks. A never-ready, terminally failed, or already-exited child does not receive the dispose request.
Model 08 | Deliberate shutdown
Disposal is ordered and bounded.
Amp withdraws capabilities first, asks an eligible child to dispose, then escalates only while the child remains alive.
Causal checkpoint
Amp withdraws capabilities before disposal. The request is conditional, cleanup is best-effort, and SIGKILL is not implied for every shutdown.
- 1. Withdraw.
Amp removes capabilities from active registries.
- 2. Dispose if eligible.
The live, ready child runs callbacks and acknowledges.
- 3. Wait.
Amp waits for acknowledgement, exit, or 3 seconds.
- 4. Escalate if alive.
SIGINT, another wait up to 3 seconds, then conditional SIGKILL.
Once the child ends, the active relationship is over. This sequence does not guarantee that in-flight work completed, every resource was released, or every callback succeeded.
The official Plugin API reference documents that onDispose runs on unload, reload, and graceful shutdown, is bounded to about three seconds, and does not run after a crash or SIGKILL. The exact parent-side request and termination sequence shown here was verified from the versioned CLI bundle.
09 | Where does this model apply?
Similar names, different systems
First separate how an extension is distributed from where it executes. Global plugins differ from ordinary plugins in distribution and scope, but "global" does not mean "server-side." They still run in the user's environment, on the client/orb side. Distribution, scope, and execution location are separate properties.
Declarative server-only agents and tools are different. They form a distinct system and should not be described as ordinary plugins with another option. The child-process lifecycle in this article should not be copied onto them. Their implementation details are outside this explanation.
Model 09 | Runtime boundary
Similar names, different systems.
Extension type, distribution or scope, and execution side answer different questions.
Causal checkpoint
Global does not mean server-side. Distribution, scope, and execution location remain separate dimensions.
| Extension | Scope | Execution |
|---|---|---|
| Ordinary plugin | Project or machine-local in this comparison | Client/orb side |
| Global plugin | Personal or workspace | Client/orb side |
| Declarative server-only agent/tool | Separate declaration system | Server side |
This final comparison tests the mental model rather than expanding it into a product taxonomy. For an ordinary plugin, we can ask where it runs, how it exchanges messages, when it is ready, who renders its UI requests, and what happens at failure or shutdown. Other extension mechanisms need their own answers.
The official Global Plugins and Skills guide defines global behavior through availability and scope. The Plugin API reference documents declarative server-only agents and tools. Those sources support the comparison boundary, not every internal runtime detail.
Official documentation
Versioned internal verification
Process, transport, handshake, reverse-RPC direction, restart behavior, Amp-owned registry withdrawal, and the exact termination sequence were checked against stable strings and structural relationships in the normalized Amp CLI bundle 0.0.1787888254-g2bce74, build 2026-08-28.
Public documentation does not state all of those internals. The bundle analysis avoids relying on minified local names and makes no claim beyond that version.