Neo Plugin System
What plugins do
Plugins are self-contained bundles that extend Neo with new capabilities and integrations. Each plugin can combine MCP server configurations, reusable skills, slash commands, event hooks, and output styles into a single installable unit. They are the primary mechanism for connecting Neo to external systems — Notion, GitHub, Google services, and similar — and for distributing persona or workflow bundles that others can install and use. When Neo loads a plugin, its contributions are merged into the live registries so that MCP tools, skills, and styles become available in the same session without any manual wiring.
Quick start — enable the Notion pilot plugin
# 1. Copy the plugin folder into ~/.neo/plugins/
cp -r templates/plugins/notion ~/.neo/plugins/
# 2. Add it to your enabled list (in ~/.neo/config.json):
# {
# "enabledPlugins": ["notion"]
# }
# 3. Get a Notion API token (see the skill that ships with the plugin)
# 4. Restart NeoAfter restarting, run /plugins list to confirm the plugin loaded successfully.
Plugin structure
A plugin is a directory. The only required file is plugin.json; everything else is optional:
my-plugin/
├── plugin.json # manifest (required)
├── skills/ # bundled skills (optional)
│ └── my-skill/SKILL.md
├── commands/ # bundled slash commands (optional, v1 deferred)
├── hooks.json # hook configs (optional)
└── output-styles/ # output styles (optional)Neo searches for plugins in:
~/.neo/plugins/— user-level installs.neo/plugins/relative to the current project root — project-level installs
Only plugins listed in enabledPlugins are loaded. All others are ignored at startup.
The plugin.json manifest
The manifest declares the plugin's identity and lists its contributions. Fields:
| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string (kebab-case) | Must be unique across all loaded plugins |
version | no | string (semver) | e.g. "1.0.0" |
description | no | string | Shown in /plugins list |
author | no | string | Attribution only |
mcpServers | no | record or path | MCP server definitions; most plugins use this |
skills | no | string[] | Paths to skill directories relative to the plugin root |
commands | no | string[] | Paths to command definitions (v1: listed but not executable) |
outputStyles | no | string[] | Paths to output style files |
hooks | no | object | Inline hook configuration |
settings | no | object | Default settings contributed by the plugin |
Explicitly skipped in v1: agents, lspServers, userConfig, dependencies, .mcpb bundles. These fields are reserved but not processed; including them has no effect and will not cause a validation error.
Complete example — Notion plugin
{
"name": "notion",
"version": "0.1.0",
"description": "Notion workspace integration via MCP",
"author": "Neo",
"mcpServers": {
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"OPENAPI_MCP_HEADERS": "{\"Authorization\": \"Bearer $NOTION_API_KEY\", \"Notion-Version\": \"2022-06-28\"}"
}
}
},
"skills": ["skills/notion-search"]
}The mcpServers record follows the same shape as the mcpServers block in ~/.neo/config.json. Each entry is a server definition with command, args, and optional env.
The /plugins slash command
/plugins list
Lists all loaded plugins with their name, version, description, and contribution counts.
/plugins listExample output:
Loaded plugins (1):
notion v0.1.0 Notion workspace integration via MCP
MCP servers: 1 Skills: 1/plugins status <name>
Shows full details for a single plugin: manifest fields, resolved file paths, and the status of each registered contribution.
/plugins status notion/plugins enable <name>
Adds the named plugin to enabledPlugins in your settings file. The plugin directory must exist under a known search directory. Requires a restart to take effect — live reload without restart is post-v1.
/plugins enable notion/plugins disable <name>
Removes the named plugin from enabledPlugins. Takes effect on the next restart.
/plugins disable notionPlugin contribution model
When Neo loads a plugin at startup, each contribution type is registered as follows:
MCP servers
MCP server definitions from mcpServers are passed to the live McpServerRegistry. The system prompt's tool-awareness section picks them up automatically — no manual configuration is needed. The plugin's servers appear alongside any servers configured directly in ~/.neo/config.json.
Each tool a server exposes is registered under the name mcp-<server>-<tool> (e.g. a calendar server's list_events tool becomes mcp-calendar-list-events).
Permissioning a connector's tools
You do not have to list every tool of a connector individually. Permission rules support a namespace wildcard on the tool name, so one rule grants or denies a whole server's tool surface:
{
"permissions": {
"allow": ["mcp-calendar-*"],
"ask": ["mcp-gmail-*"],
"deny": ["mcp-calendar-delete-event"]
}
}mcp-calendar-*matches every tool from thecalendarserver.- A bare
mcp-*matches every MCP tool from every server. - A more specific rule still wins by the usual precedence (
deny>ask>allow), so the example auto-allows all calendar tools exceptdelete-event, which is denied. - A namespace rule is tool-wide — it ignores any
(content)suffix.
This mirrors Claude Code's mcp__server__* server-level grant. On untrusted channels the mobile profiles still demote any non–pre-authorised tool (including connector tools) to ask, so a namespace allow in your config does not weaken the channel clamp.
Skills
Skills listed under skills/ are loaded and added to the SkillRegistry at a tier between user and project. This means a project-level skill with the same name takes precedence over a plugin skill, and a plugin skill takes precedence over a user-level skill. Skills are invoked with the / command prefix just like any other skill.
Output styles
Output style files are registered with the OutputStylesLoader and become available as named styles within the session.
Commands (v1 deferred)
Command files in the commands/ directory, and paths listed in the commands field of plugin.json, are recorded in the manifest at load time. However, the markdown command parser that would make them executable is deferred to a post-v1 release. Bundled commands will appear in /plugins status output but cannot be invoked yet.
Hooks (v1 deferred)
Hook configurations in hooks.json or in the hooks field of plugin.json are recorded in the manifest. The hook merge path that applies plugin-scoped hooks to the session event bus is deferred to post-v1. Plugin hooks will not fire in the current release.
Authoring your own plugin
- Create a directory under
~/.neo/plugins/<your-name>/. - Write
plugin.jsonwith at minimum anamefield and one contribution (an MCP server, a skill, an output style, or a combination). - Add to
enabledPluginsin~/.neo/config.json:json{ "enabledPlugins": ["your-name"] } - Restart Neo.
- Verify with
/plugins list— your plugin should appear with its contribution counts.
A minimal plugin that adds a single MCP server needs only a plugin.json with name and mcpServers. Skills and output styles can be added incrementally.
When writing a skill for your plugin, place it under skills/<skill-name>/SKILL.md inside the plugin directory, then reference the path in the skills array in plugin.json. The skill's YAML frontmatter follows the same format as any other Neo skill — description, when_to_use, model, allowed-tools, and hooks are all recognized fields.
Troubleshooting
PluginNotFoundError
The directory named in enabledPlugins does not exist under any of the plugin search paths (~/.neo/plugins/ or .neo/plugins/ in the project root). Check the directory name matches exactly (case-sensitive on Linux/macOS).
PluginManifestInvalidError
plugin.json failed schema validation. The error message includes a issues array from Zod with each failing field and reason. Common causes: name is missing or contains spaces, mcpServers entries are missing a command field, or a skills path points to a file rather than a directory.
PluginDuplicateNameError
Two plugins in the enabled list declare the same name field in their plugin.json. Each plugin must have a unique name. Rename one of the conflicting plugins to resolve this.
MCP server does not connect
Plugin loading only registers the server definition — the actual MCP connection is established separately when Neo starts up tool discovery. If the server fails to connect, the error will appear in the session startup log rather than as a plugin load error. Check:
- Required environment variables are set (e.g.,
NOTION_API_KEYfor the Notion plugin). - The
commandis on the system PATH (e.g.,npxfor Node-based MCP servers). - The MCP server package is accessible (run the command manually to verify).
Deeper references
For implementers and plugin authors who need the canonical v1 contract:
docs/internal/plugins/architecture.md— canonical v1 architecture reference. Manifest schema, dispatch matrix, source priority across tiers, error containment, bootstrap dependency injection. Every claim citesfile:line. Start here when extending the plugin subsystem.docs/user/plugins/skill-hooks.md— Neo-unique skill-hook subsystem deep dive: the contract, fail-open semantics, when to use vs the other four hook backends.docs/user/plugins/README.md— extended user-facing reference (slash commands, MCP servers inside a plugin, hooks inside a plugin, skills inside a plugin, worked end-to-end Notion example).docs/user/plugins/cc-compatibility.md— which Claude Code manifest fields Neo consumes vs accepts-but-ignores; how to install a CC plugin from GitHub.
Design references
plans/wave5-plugin-foundation.md— Wave 5 design doc (in-process lifecycle, plugin permissions, manifest v2, PoC migration). Source of architecture.md.plans/wave4/cc-plugin-system.md— architectural research: how Claude Code structures its plugin model and the design decisions adopted or adapted for Neo.plans/wave4/integration-style-matrix.md— rationale for why a plugin system was chosen over alternative integration approaches (inline config, per-tool adapters, etc.).scratch/wave5-ledger.md— Wave 5 build log tracking all plugin system implementation work.