Plugin
June 11, 2026 · View on GitHub
What it is
A plugin modifies the connections between modules, not the modules themselves. Modules are the blocks. Plugins are what runs at the seams.
There are two flavours, each solving a different problem:
- Prompt plugins contribute content to the system prompt when the controller builds it.
- Lifecycle plugins hook into runtime events: before/after an LLM call, before/after tool dispatch/execution, before/after a sub-agent spawn, and at lifecycle checkpoints like compaction and interrupts.
Together, plugins are the main way to add behaviour without forking any module.
Why it exists
Most useful agent behaviours are not a new tool and not a new LLM; they are a rule that runs between them. Examples:
- "Before every bash call, check it against a safety policy."
- "After every LLM call, count tokens for billing."
- "Before every LLM call, retrieve relevant past events and inject them into the messages."
- "Always prepend a project-specific instruction section to the system prompt."
Each of these could be done by subclassing a module. That is invasive and fragile: you fork, someone upstream ships a change, you rebase. Plugins let you hook the seams without touching the blocks.
How we define it
Prompt plugins
A BasePlugin subclass with:
- a
nameandpriority(lower = earlier in the prompt), - a
get_content(context) → str | Nonethat returns a section of prompt text (orNoneto contribute nothing).
The aggregator (prompt/aggregator.py) sorts registered plugins by
priority and concatenates their outputs into the final system prompt.
Built-ins: ToolListPlugin (auto tool index), FrameworkHintsPlugin
(how to call tools / use ##commands##), EnvInfoPlugin (working
dir, date, platform), ProjectInstructionsPlugin (loads
CLAUDE.md / .claude/rules.md).
Lifecycle plugins
A BasePlugin subclass with any of these hooks:
on_load(context),on_unload()should_apply(context) -> boolor declarativeapplies_to = {agent_names, model_patterns}contribute_commands()for custom##command##handlerscontribute_termination_check()for pluggable stop conditionspre_llm_call(messages, **kwargs) → list[dict] | Nonepost_llm_call(messages, response, usage, **kwargs) → str | Nonepre_tool_dispatch(call, context) → ToolCallEvent | Nonepre_tool_execute(args, **kwargs) → dict | Nonepost_tool_execute(result, **kwargs) → ToolResult | Nonepre_subagent_run(task, **kwargs) → str | Nonepost_subagent_run(result, **kwargs) → Any | None- Fire-and-forget callbacks such as
on_agent_start,on_agent_stop,on_event,on_interrupt,on_task_promoted,on_compact_start,on_compact_end.
A pre_* hook can raise PluginBlockError("message") to abort the
operation; the message becomes the tool result or a blocked
tool_complete event. post_llm_call rewrites are special: when a
plugin changes the final assistant text, the runtime emits an
assistant_message_edited activity marker so UIs can show that the text
was modified after generation.
How we implement it
PluginManager filters plugins by enable/disable state and by
should_apply(context) before each hook call. bootstrap/plugins.py
loads config-declared plugins on agent start; package-declared plugins
are discoverable via kohaku.yaml.
Two newer extension points live alongside the hook surface:
- Controller commands. Plugins and packages can register custom
##name##commands into the controller namespace. - Termination voters. Plugins can contribute a checker that receives
a
TerminationContext; any checker can stop the run.
What you can therefore do
- Safety guards. A
pre_tool_executeplugin that rejects dangerous commands. - Token accounting.
post_llm_callcounting tokens and writing to an external store. - Seamless memory.
pre_llm_callrunning an embedding lookup over past events and prepending relevant context, essentially RAG over session history without tool calls. - Smart guard. A
pre_tool_executeplugin that runs a small nested agent to decide whether the action is acceptable. Plugins are Python, and agents are Python, so this is legal. See patterns. - Prompt composition. A prompt plugin that injects dynamic instructions derived from scratchpad state or session metadata.
Don't be bounded
Plugins are optional. A creature with no plugins works fine. But when you find yourself thinking "I need a new kind of behaviour everywhere in the loop," the answer is almost always a plugin, not a new module.
See also
- Controller: where the hooks fire.
- Prompt aggregation: how prompt plugins slot in.
- Smart guard and seamless memory in Patterns: agent-inside-plugin.
- reference/plugin-hooks.md: every hook signature.