Plugins¶
Plugins extend agents with reusable skills, commands, agents, hooks, and MCP
servers. A plugin lives inside the owning agent's repo
(/workspace/plugins/<name>) or in a shared repo
(/repos/knowledge/plugins/<name>) and is managed with the plugin CLI
(scripts/tri-onyx-plugin.py).
How plugins reach agents¶
-
Agent definition declares plugins in frontmatter:
-
Gateway resolves each plugin to a directory inside a mounted repo — either the agent's own repo (
/workspace/plugins/<name>) or a shared repo the agent has inrepos_read/repos_write(/repos/knowledge/plugins/<name>). Access follows the repo mount: no mount, no plugin. -
Agent runner passes plugin paths to the Claude Agent SDK:
-
SDK passes
--plugin-dir /workspace/plugins/newsaggto the bundled Claude Code CLI, which discovers the plugin manifest and registers components (skills, commands, hooks, etc.). -
Skill invocations use the
/<plugin>:<skill>syntax (e.g.,/newsagg:fetchnews). The CLI expands the skill'sSKILL.mdinto the prompt and sends it to the model.
Plugin directory structure¶
Each plugin follows the Claude Code plugin convention:
newsagg/
├── .claude-plugin/
│ └── plugin.json # Plugin manifest (name is required)
├── skills/ # Auto-discovered by convention
│ └── fetchnews/
│ └── SKILL.md # Skill definition
├── commands/ # Slash commands (optional)
├── agents/ # Subagent definitions (optional)
├── hooks/ # Event handlers (optional)
│ └── hooks.json
├── .mcp.json # MCP servers (optional)
└── ... # Plugin-specific files
plugin.json¶
Minimal manifest — only name is required:
Component directories (skills/, commands/, agents/, hooks/) are
auto-discovered at their default locations. You only need to declare them in
plugin.json if they live at non-standard paths.
Path rules (important)¶
The Claude Code CLI enforces strict path rules for plugin manifests:
- All custom paths must start with
./— relative to the plugin root. Paths starting with../are invalid and will cause the CLI to silently fail to load the plugin. - Default directories are auto-discovered —
skills/,commands/,agents/,hooks/don't need to be declared inplugin.json. - Custom paths supplement defaults — declaring a custom
skillspath adds to the defaultskills/directory, it doesn't replace it.
Example of what NOT to do:
This fails silently because ../skills/ doesn't start with ./. Since
skills/ is auto-discovered anyway, the fix is to omit the field entirely.
Gateway: system command handling¶
Messages starting with / are intercepted by the gateway's SystemCommand
module before reaching agents. Known commands (e.g., /restart) are executed
directly. Unknown commands return an error.
Skill invocations use colons (/newsagg:fetchnews) to distinguish
themselves from system commands. The parser detects the : and passes the
message through to the agent instead of intercepting it:
# system_command.ex — parse/1
:error ->
if String.contains?(name, ":") do
:not_a_command # Skill — pass through to agent
else
{:command, :unknown, ["/" <> name]} # Unknown system command — error
end
Debugging plugin loading¶
If a skill invocation returns 0 turns, check these in order:
-
Is the plugin loaded? Look at the SDK
SystemMessageinit data in container logs. Thepluginsarray should contain your plugin, andslash_commandsshould list<plugin>:<skill>. -
Is the manifest valid? Ensure
.claude-plugin/plugin.jsonhas valid JSON and no paths starting with../. -
Is the plugin directory mounted? The agent definition must list the plugin in
plugins:, and the plugin must live in the agent's own repo (/workspace/plugins/<name>) or in a shared repo the agent mounts viarepos_read/repos_write. Check inside the container that the directory exists and is readable. -
Is the gateway passing the message through? Messages with
:in the command name should reach the agent. Check gateway logs if the agent never receives the prompt. -
Check container logs directly:
Unhandled SDK message types (includingSystemMessage) are logged as warnings by the agent runner.
Managing plugins¶
# Install a plugin from a git repo
uv run scripts/tri-onyx-plugin.py add <git-url> [--name NAME]
# List installed plugins
uv run scripts/tri-onyx-plugin.py list
# Upgrade a plugin
uv run scripts/tri-onyx-plugin.py upgrade <name>
# Remove a plugin
uv run scripts/tri-onyx-plugin.py remove <name>
Plugins are installed into the target repo (an agent's own repo or a shared
repo such as knowledge) — there is no global plugin directory or manifest.
When installed from git, the .git/ directory is stripped so the files become
regular content of the owning repo, versioned by the gateway's per-session
commits.