How plugins work
Plugins hang extension points on session and model-call boundaries: change messages, system, sampling params and headers, tool definitions, and logic before/after tool execute. Load is cached per project directory instance; same-named hooks rewrite one shared output in load order — order is deterministic.
Load and scope
Section titled “Load and scope”On first enter of an Instance for a directory, load once: register builtin plugins first (often provider auth), then resolve plugin sources from config (npm or local path), install and import, then register hooks. Disable by id; pure mode can skip external plugins. After config change, destroy and rebuild the Instance to reload — not in-process hot swap.
Factory functions usually receive: a loopback HTTP client (same-process calls to local APIs), current project / directory / worktree, service URL, and optional shell capability. After load, each plugin’s config hook may run, and the internal event bus may be subscribed.
Trigger model
Section titled “Trigger model”trigger(name, input, output): call in load order; later hooks see earlier rewritten output. Non-trigger forms also exist: auth methods, append model lists, register custom tools.
Timing relative to the main path
Section titled “Timing relative to the main path”flowchart TD In[Assemble user input] --> CM[chat.message] CM --> Store[Persist user message] Store --> RL[runLoop] RL --> TD[tool.definition] RL --> MT[messages.transform] RL --> ST[system.transform] ST --> CP[chat.params] CP --> CH[chat.headers] CH --> LLM[streamText] LLM --> TB[tool.execute.before] TB --> EX[execute] EX --> TA[tool.execute.after]| Hook | Timing |
|---|---|
chat.message | After user message assembly, before persist; only then enters runLoop (noReply stores only) |
command.execute.before | After command expands to parts, then the same prompt path |
tool.definition | Before each turn’s tool table is given to the model |
experimental.chat.messages.transform | Rewrite history before each model send |
experimental.chat.system.transform | Rewrite system before entering the LLM stream |
chat.params | Change temperature and other sampling / provider options |
chat.headers | Change HTTP headers to the model gateway |
tool.execute.before / after | Around real execute |
shell.env | Inject env vars before shell execute |
experimental.text.complete | When an assistant text Part closes |
experimental.session.compacting | Before compaction starts (may append or replace compaction prompt) |
event | Decoupled from prompt: async bus notifications |
Permission asks go through the internal permission module and bus events (SSE can receive them); they do not depend on unwired permission hook names. For integrations, treat the table above as the hooks that actually trigger.
Boundaries
Section titled “Boundaries”- Keep hooks short; heavy I/O inside
tool.execute.*lengthens tool-turn latency. - Serial order follows load order; when several plugins rewrite the same field, self-test the override result.
- Custom tools registered via plugin
toolstill pass registry and permission filters. See Tool system.
Related reading: