Plugins
A plugin is a Python file with a register(slo) function. Slo calls it when a session starts, and the plugin declares what it adds: a tool the model can call, a slash command, a custom renderer, a footer status, or a rule that checks every tool call before it runs.
Plugins are how you teach Slo things that aren't prompts. Block git push --force to main. Redact tokens before the model ever sees them. Add a /deploy-status command for your team.
What a plugin can do
| Interface | What it does |
|---|---|
slo.on(event_kind, handler) | Watch the live event stream: text, thinking, tool calls, tool output, run state, sub-agents. |
slo.register_tool(tool) | Add a tool the model can call. Built-in tools can't be replaced. |
slo.register_command(name, handler) | Add a slash command for you to run in the TUI. |
slo.register_tool_renderer(tool_name, render) | Change how a tool's output is displayed. The model still sees the original. |
slo.set_status(key, text) | Show a short status in the footer. Empty text clears it. |
slo.on_before_tool_call(handler) | Inspect a tool call before it runs. Block it with a reason, or rewrite its input. |
slo.on_after_tool_result(handler) | Change a tool result before the model sees it, for example to redact secrets. |
slo.on_before_agent_turn(handler) | Add context messages before each model turn. |
Plugins can't add new agent types, own screens or modals, handle sign-in, or run background workers.
A small example
This plugin blocks force-pushes to main and shows that it's active:
from typing import Any
from src.agents.slo_agent.plugins.dao import BeforeToolCallDecisionDAO
BLOCKED = ("git push --force origin main", "git push -f origin main")
def register(slo: Any) -> None:
slo.set_status(key="push-guard", text="Push guard: on")
slo.on_before_tool_call(handler=block_force_push)
def block_force_push(decision: BeforeToolCallDecisionDAO, tool_input: dict) -> BeforeToolCallDecisionDAO:
command = str(tool_input.get("command", ""))
if any(pattern in command for pattern in BLOCKED):
return BeforeToolCallDecisionDAO(block=True, reason="Force-pushing to main is not allowed here.")
return BeforeToolCallDecisionDAO()
Interceptors run in the order they were registered, and the first one that blocks wins. The full interface reference, with every callback signature, lives in docs/EXTENSIONS.md in the Slo repository.
Three ways to load a plugin
1. While you write it. Point Slo at the file. Explicit paths are trusted, and you can repeat the flag:
slo --plugin ./my_plugin.py
slo run --plugin ./my_plugin.py -p "Tidy up the release notes"
2. For one project. Put it in .slo/plugins/ inside the project. Slo won't load project plugins until you trust the project, so a repository you just cloned can't run code on your machine by surprise:
/plugins trust trust this project and load its plugins
/plugins reload reload after editing a plugin file
/plugins show each plugin's health: loaded, load_failed or disabled
3. Installed from GitHub or GitLab. Managed installs are pinned to an exact commit:
slo plugin install https://github.com/acme/deploy-tools --yes
slo plugin list
slo plugin upgrade acme-deploy-tools
slo plugin uninstall acme-deploy-tools
The plugin id comes from the URL (owner-repo). Installing never runs the plugin's code, only copies the files its manifest lists. If an upgrade fails partway, Slo restores the previous version. Sources must be https://github.com/owner/repo or https://gitlab.com/owner/repo.
Load order is installed plugins, then trusted project plugins, then --plugin paths.
Turning plugins off
- For one run:
SLO_PLUGINS_DISABLED=1 slo. The value has to be exactly1. Sub-agents inherit it. - For good:
slo plugin uninstall <id>, or delete the file from.slo/plugins/.
What stays out of the model's context
Renderer output, footer status and live tool output snapshots are for your eyes only. A plugin reaches the model through three routes: a tool it registers, a tool result it rewrites, or a message it adds before a turn.
When a plugin misbehaves
A plugin that throws, or an interceptor that takes longer than 5 seconds, is treated as having no opinion. The run carries on and the failure shows up in /plugins. Raise the limit with SLO_PLUGIN_INTERCEPTION_TIMEOUT_SECONDS if a check genuinely needs longer. Event watchers registered with slo.on have no timeout, so keep them quick.