Follow one small plugin from a TypeScript file to a command a coding agent can actually use.
A plugin can make a coding agent feel as if it learned something new in an instant. You install one, a command appears, and it works. But the agent still has to find the plugin, start it, learn what it provides, and keep talking to it while that command runs.
To make that process concrete, we will use Amp, one of the best coding agents out there, and follow one of its plugins from start to finish. This is a close look at Amp's child-process model, not a claim that every coding agent loads plugins in exactly the same way.
Let us start from the very beginning and follow one plugin step by step. Our plugin asks the user to choose a fruit and then shows the selection in a notification. That is all it does.
Real plugins can add commands, tools, skills, event handlers, and modes. A small example is more useful here because we can see every part of the process.
01 The plugin file
The user creates fruit.ts in a place where Amp looks for plugins. The file registers one command called fruit.pick. When the command runs, it opens a selector and waits for the user to choose a fruit. Then it shows the result.
import type { PluginAPI } from "@ampcode/plugin";
export default function (amp: PluginAPI) {
amp.registerCommand(
"fruit.pick",
{
title: "Pick a fruit",
description: "Choose a fruit and show it",
},
async (ctx) => {
const fruit = await ctx.ui.select({
title: "Pick a fruit",
options: ["Apple", "Banana", "Cherry"],
});
if (fruit) {
await ctx.ui.notify(`You chose ${fruit}.`);
}
},
);
}The file is easy to read, but the command does not exist in Amp yet. Amp still has to find the file, start the plugin, and learn what it registered.
02 The agent starts another process
In our example, plugin loading starts when the Amp client starts. The user runs amp, and Amp discovers fruit.ts.
This gives us two processes: the main Amp process and a child process for the fruit plugin. Each ordinary plugin gets its own child process.
Amp creates the child by making a temporary directory, writing its plugin runtime there, and executing its Bun binary again with BUN_BE_BUN=1. It passes the runtime script and the path to fruit.ts to that new process.
Amp keeps a handle to the child process. Through that handle it can write to the child's standard input and read from its standard output.
03 The two processes communicate
Amp uses stdin and stdout to exchange messages with the plugin process.
Amp uses its own RPC messages, encoded as one JSON object followed by a newline. The object describes a request, response, or event; the newline marks where one message ends and the next begins. Here, RPC describes the request-and-response pattern, not a claim that Amp uses the formal JSON-RPC wire protocol.
The format is less important than the picture it gives us. There are two processes, and anything they need from each other has to cross this connection as a message.
04 The child loads the plugin
The child does not import fruit.ts immediately. First it asks Amp for information about the client it is joining.
Amp replies with its server address, the workspace root, the kind of executor running the plugin, and the authenticated user when one is available.
Only then does the child import the plugin file. It takes the default export and calls it with an amp instance created by the runtime. This is the same amp value used by amp.registerCommand in our example.
While the child imports the file and runs setup, Amp waits up to 10 seconds for a ready message. If that message does not arrive, Amp treats the load as timed out. A plugin file can exist and even import successfully without becoming a working capability.
Swipe or use the arrow keys to see the full sequence.
sequenceDiagram
participant Amp as Amp process
participant Plugin as Fruit plugin process
Plugin->>Amp: Request client information
Amp-->>Plugin: Server, workspace, user, executor kind
Plugin->>Plugin: Import fruit.ts
Plugin->>Plugin: Call default export with amp
Both the import and the function call happen inside the child. Amp never sources fruit.ts into its main process.
05 The agent learns what the plugin added
When fruit.ts calls registerCommand, the runtime stores the command and its handler inside the child process. It then tells the main Amp process that the command list changed, and plugin setup continues.
After the plugin's default function finishes, the child sends a ready message. Amp can now ask for every command, tool, skill, event, and mode the plugin registered. For our example, Amp receives fruit.pick, adds it to the command palette, and connects that command to the fruit plugin process. That connection is how Amp knows where to send the request when the user runs it.
Swipe or use the arrow keys to see the full sequence.
sequenceDiagram
participant Amp as Amp process
participant Plugin as Fruit plugin process
Plugin->>Plugin: Store fruit.pick and its handler
Plugin-)Amp: Command list changed
Note right of Plugin: Plugin setup continues
Plugin-)Amp: Plugin is ready
Amp->>Plugin: Request registered capabilities
Plugin-->>Amp: fruit.pick and other registrations
Note left of Amp: Bind fruit.pick
06 The user runs the command
The user opens the command palette and runs "Pick a fruit." Amp knows that fruit.pick belongs to our plugin, so it sends a request to run the command to that child process.
The child finds the command handler and calls it with a context. The first interesting line is ctx.ui.select(...). The plugin cannot draw the Amp interface itself, so this call sends a request back to the main process.
Amp shows the selector. The user chooses Banana, and Amp sends that value back to the promise waiting inside the plugin. The next line calls ctx.ui.notify(...). That becomes another request to Amp, which displays the notification. The plugin can then finish the original command request.
Swipe or use the arrow keys to see the full sequence.
sequenceDiagram
actor User
participant Amp as Amp process
participant Plugin as Fruit plugin process
User->>Amp: Run Pick a fruit
Amp->>Plugin: Run fruit.pick
Plugin->>Amp: Show a fruit selector
Amp->>User: Apple, Banana, or Cherry?
User-->>Amp: Banana
Amp-->>Plugin: Banana
Plugin->>Amp: Show You chose Banana
Amp->>User: Display notification
Amp-->>Plugin: Notification displayed
Plugin-->>Amp: Command finished
The plugin decides what it wants to ask for. Amp owns the interface and the interaction with the user. Both directions travel through the same RPC connection.
07 What happens when it fails?
A failure before the ready message is a load failure. Amp may have started the process and imported the file, but the plugin did not finish startup. Its command never becomes available.
A crash after readiness is different because the plugin was already active. Amp removes its capabilities and can restart the child up to three times after the initial run. The attempts use backoff instead of repeating immediately in a tight loop.
The process boundary matters here. An exception in the fruit plugin does not run inside Amp's main process. Amp sees the child exit and handles that plugin as failed.
08 How does it stop?
During a normal unload, reload, or shutdown, Amp first removes the plugin's capabilities. If the child reached readiness and is still running, Amp asks it to clean up its resources and reply.
Amp waits about three seconds for that reply. If the child stays alive, Amp asks the operating system to interrupt it, waits again, and can force the process to stop as the final step. A plugin that crashes or is forced to stop does not get a chance to clean up.
09 Why use this model?
Amp keeps each ordinary plugin across a process boundary and communicates with it through RPC. That boundary fits Amp's executor model: the plugin can run where the executor is running, on the client or in an Orb, while Amp continues to communicate with it through messages.
Global plugins change how a plugin is distributed and who can use it, but they still execute on the client or Orb side.
Amp also has declarative server-only agents and tools. They are a separate system, so the child-process lifecycle in this article should not be applied to them.
The supported plugin surface and lifecycle boundaries are documented in the Amp Plugins guide and the Plugin API reference.
A plugin is not just a file that becomes part of an agent. It has a lifecycle and a working relationship with the system that loaded it.
In Amp, that relationship lives across a process boundary and is carried by data and messages. The same model repeats for every ordinary plugin it loads. Add another plugin and Amp starts another child process, with its own connection, readiness state, capabilities, and failure boundary.
That is the useful picture to keep. The plugin gives the agent another capability without becoming part of its main process. Amp starts it, talks to it, and makes its commands or tools available once the plugin is ready.
