Skip to content

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.

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(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.

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]
HookTiming
chat.messageAfter user message assembly, before persist; only then enters runLoop (noReply stores only)
command.execute.beforeAfter command expands to parts, then the same prompt path
tool.definitionBefore each turn’s tool table is given to the model
experimental.chat.messages.transformRewrite history before each model send
experimental.chat.system.transformRewrite system before entering the LLM stream
chat.paramsChange temperature and other sampling / provider options
chat.headersChange HTTP headers to the model gateway
tool.execute.before / afterAround real execute
shell.envInject env vars before shell execute
experimental.text.completeWhen an assistant text Part closes
experimental.session.compactingBefore compaction starts (may append or replace compaction prompt)
eventDecoupled 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.

  • 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 tool still pass registry and permission filters. See Tool system.

Related reading: