Plugin hooks
August 15, 2026 · View on GitHub
Every lifecycle, LLM, tool, sub-agent, and callback hook exposed to
plugins. Hooks are defined by the Plugin protocol in
kohakuterrarium.modules.plugin; BasePlugin gives you default
no-op implementations. Wired in bootstrap/plugins.py.
For the mental model, read concepts/modules/plugin. For task-oriented walkthroughs, see guides/plugins and guides/custom-modules.
Return-value semantics
- Transform hooks (
pre_*,post_*): returnNoneto keep the value unchanged, or return a new value to replace the input going into the next plugin / the framework. - Callback hooks (
on_*): return value is ignored; they are fire-and-forget.
Blocking
Any pre_* hook may raise PluginBlockError to short-circuit the
operation. The framework surfaces the error, the request does not
proceed, and the matching post_* hook is not fired. Callback
hooks cannot block.
Lifecycle hooks
| Hook | Signature | Fired when | Return |
|---|---|---|---|
on_load | async on_load(ctx: PluginContext) -> None | Plugin is loaded into an agent. | ignored |
on_unload | async on_unload() -> None | Plugin is unloaded or agent stops. | ignored |
should_apply | def should_apply(ctx: PluginContext) -> bool | Evaluated before each hook call. | False skips this plugin for that context. |
contribute_commands | def contribute_commands() -> dict[str, BaseCommand] | After load, during controller wiring. | Mapping of controller command names. |
contribute_termination_check | def contribute_termination_check() -> Callable[[TerminationContext], TerminationDecision | None] | None | During termination wiring. | Checker function or None. |
PluginContext gives the plugin access to the host agent, session store,
scratchpad, registry, controller, subagent manager, and helper methods like
switch_model(...), inject_event(...), and inject_message_before_llm(...).
Plugins can also declare a static filter with applies_to = {agent_names, model_patterns}.
LLM hooks
| Hook | Signature | Fired when | Return semantics |
|---|---|---|---|
pre_llm_call | async pre_llm_call(messages: list[dict], **kwargs) -> list[dict] | None | Before every LLM request (controller, sub-agent, compact). | None keeps the list; a new list replaces it. May raise PluginBlockError. |
post_llm_call | async post_llm_call(messages: list[dict], response: str, usage: dict, **kwargs) -> str | None | After the final assistant message is assembled. | None keeps the text; a returned string rewrites it for the next plugin / final output. |
When a post_llm_call rewrite changes the final assistant text, the runtime emits an assistant_message_edited activity marker so UIs can audit that rewrite.
Tool visibility contributions
Plugins can restrict the catalog exposed to the model on each native
request without touching the registry. PluginManager intersects
multiple contributions per category, so each plugin can only narrow the
catalog.
| Hook | Signature | Fired when | Return semantics |
|---|---|---|---|
get_tool_visibility | def get_tool_visibility(context: PluginContext) -> ToolVisibility | None | Before each native request's tool schemas and provider-native tools are built. | None keeps the catalog unrestricted. A ToolVisibility(allowed_tools=..., allowed_subagents=...) keeps only the named members; an empty frozenset hides the whole category. |
ToolVisibility fields:
allowed_tools: frozenset[str] | None—Nonemeans unrestricted.allowed_subagents: frozenset[str] | None—Nonemeans unrestricted.
This contribution only affects native tool schemas; text-mode prompt filtering is left to prompt contributions.
Tool hooks
| Hook | Signature | Fired when | Return semantics |
|---|---|---|---|
pre_tool_dispatch | async pre_tool_dispatch(call: ToolCallEvent, context: PluginContext) -> ToolCallEvent | None | After parsing, before executor submission. | None keeps the call; a returned event rewrites tool name/args. May raise PluginBlockError. |
pre_tool_execute | async pre_tool_execute(args: dict, **kwargs) -> dict | None | Just before tool execution. | None keeps args; a new dict replaces them. May raise PluginBlockError. kwargs include tool_name, job_id. |
post_tool_execute | async post_tool_execute(result: ToolResult, **kwargs) -> ToolResult | None | After a tool completes (including error results). | None keeps the result; a new ToolResult replaces it. kwargs include tool_name, job_id, args. |
Sub-agent hooks
| Hook | Signature | Fired when | Return semantics |
|---|---|---|---|
pre_subagent_run | async pre_subagent_run(task: str, **kwargs) -> str | None | Before a sub-agent is spawned and started. | None keeps the task; a returned string replaces it. May raise PluginBlockError. kwargs include name, job_id, is_background. |
post_subagent_run | async post_subagent_run(result: Any, **kwargs) -> Any | None | After a sub-agent completes (its output is about to be delivered as a subagent_output event). | None keeps the result; a returned value replaces it. kwargs include name, job_id. |
Callback hooks
All callbacks are fire-and-forget. Their return value is ignored.
| Hook | Signature | Fired when |
|---|---|---|
on_agent_start | async on_agent_start() -> None | agent.start() completed. |
on_agent_stop | async on_agent_stop() -> None | agent.stop() begins. |
on_event | async on_event(event: TriggerEvent) -> None | Any event is injected into the controller. |
on_interrupt | async on_interrupt() -> None | The user interrupts the agent. |
on_task_promoted | async on_task_promoted(job_id: str, tool_name: str) -> None | A direct task is promoted to background. |
on_compact_start | async on_compact_start(context_length: int) -> bool | None | Before compaction. Return False to veto this compaction cycle. |
on_compact_end | async on_compact_end(summary: str, messages_removed: int) -> None | After compaction finishes. |
Prompt plugins (separate category)
Prompt plugins run during system prompt assembly in
prompt/aggregator.py. They are loaded independently from lifecycle
plugins.
BasePlugin (in kohakuterrarium.prompt.plugins) has:
priority: int # lower = earlier
name: str
async def get_content(self, context: PromptContext) -> str | None
get_content(context) -> str | None: Return the text block to insert, orNoneto contribute nothing.priority: ordering key. Built-ins sit at 50/45/40/30.
Built-in prompt plugins are listed in Prompt plugins in builtins.md.
Register custom prompt plugins via the plugins field of a creature
config (same as lifecycle plugins); the framework dispatches based on
whether a plugin class subclasses the lifecycle Plugin protocol or
the prompt BasePlugin.
Writing a plugin
Minimal lifecycle plugin:
from kohakuterrarium.modules.plugin import BasePlugin, PluginBlockError
class GuardPlugin(BasePlugin):
async def pre_tool_execute(self, args, **kwargs):
if kwargs.get("tool_name") == "bash" and "rm -rf" in args.get("command", ""):
raise PluginBlockError("unsafe command")
return None # keep args unchanged
Register in a creature config:
plugins:
- name: guard
type: custom
module: ./plugins/guard.py
class: GuardPlugin
Enable/disable at runtime via /plugin toggle guard (see
User commands in builtins.md) or the HTTP
plugin toggle endpoint.
See also
- Concepts: plugin, patterns.
- Guides: plugins, custom modules.
- Reference: python, configuration, builtins.