Tools live inside the LLM's world. They're functions the AI can call during a conversation — search the web, save a memory, check the weather. The AI decides when to use them. If all you want is to give your AI new abilities it can call, you don't need this guide — TOOLMAKER.md covers that. The AI can even create tools for itself at runtime.
Plugins control everything else.
A plugin is a package that can contain tools, yes, but also hooks into parts of the pipeline the LLM never touches. Plugins can:
- Intercept voice input after speech-to-text, before the LLM ever sees it (
post_stt) - Filter or rewrite the AI's response before it's saved to history (
post_llm) - Inject context into the system prompt every turn, silently (
prompt_inject) - Block or modify tool arguments before execution (
pre_execute) - Control TTS — change the voice, rewrite text, or cancel speech entirely (
pre_tts) - React to wakeword detection before recording starts (
on_wake) - Run scheduled tasks on cron timers, independent of any conversation
- Register voice commands — keyword triggers that bypass the LLM entirely
- Ship a settings UI that renders in the browser with zero JavaScript (or full custom JS)
A tool is a single function the AI can call. A plugin is an autonomous package that can reshape how Sapphire behaves at every stage — input, processing, output, and beyond.
If you want to give the AI a new ability → read TOOLMAKER.md. If you want to tap into the pipeline itself → you're in the right place.
| Guide | What's Inside |
|---|---|
| Manifest | plugin.json reference — fields, priority bands, directory structure |
| Hooks | All 10 hook points, HookEvent fields, system access, examples |
| Voice Commands | Keyword triggers that bypass the LLM — match modes, handlers, macros |
| Tools | Tool file format, schema flags, scopes, reading settings, privacy patterns |
| Routes | Custom HTTP endpoints — path params, auth enforcement, handler signature |
| Schedule | Cron tasks — manifest fields, handler contract, examples |
| Settings | Manifest-declared settings, custom web UI, settings API, danger confirms |
| Web UI | Shared JS modules, CSS variables, modals, CSRF, style injection |
| Signing | Verification states, sideloading, signing your own plugins |
| Lifecycle | Startup, live toggle, hot reload, rescan, error isolation |
| AI Reference | Compact reference for Sapphire's own use when building plugins |
Minimal plugin — logs every chat:
plugins/my-plugin/
plugin.json
hooks/greet.py
plugin.json:
{
"name": "my-plugin",
"version": "1.0.0",
"description": "Logs every chat",
"author": "you",
"capabilities": {
"hooks": {
"post_chat": "hooks/greet.py"
}
}
}hooks/greet.py:
import logging
logger = logging.getLogger(__name__)
def post_chat(event):
logger.info(f"User: {event.input}")
logger.info(f"AI: {event.response}")Enable in Settings > Plugins. It loads immediately — no restart.
| Path | Band | Priority Range | Tracked |
|---|---|---|---|
plugins/ |
System | 0-99 | Yes |
user/plugins/ |
User | 100-199 | No (gitignored) |
User plugin priorities are automatically offset into the 100-199 range.
A plugin combining hooks, tools, voice commands, and a scheduled task:
plugins/smart-home/
plugin.json
hooks/context.py
hooks/quick_lights.py
tools/devices.py
routes/camera.py
schedule/lock_check.py
web/
index.js
{
"name": "smart-home",
"version": "2.0.0",
"description": "Full smart home integration",
"author": "you",
"url": "https://example.com",
"priority": 50,
"capabilities": {
"hooks": {
"prompt_inject": "hooks/context.py"
},
"voice_commands": [
{
"triggers": ["lights on", "lights off"],
"match": "exact",
"bypass_llm": true,
"handler": "hooks/quick_lights.py"
}
],
"tools": ["tools/devices.py"],
"routes": [
{
"method": "POST",
"path": "camera/{room}",
"handler": "routes/camera.py:handle_snapshot"
}
],
"schedule": [
{
"name": "Nightly Lock Check",
"cron": "0 23 * * *",
"handler": "schedule/lock_check.py"
}
],
"web": { "settingsUI": "plugin" }
}
}See each guide above for the details on every capability.