Plugin Anatomy
What's inside a plugin depends on whether the platform is manifest-first or code-first. Claude Code and Codex use manifests plus root-level plugin components. OpenCode plugins are JavaScript/TypeScript modules loaded from local files or npm. Pi's closest equivalent is a package declared in package.json that can bundle skills, extensions, prompts, and themes.
Plugin Structure
There isn't one universal plugin shape:
- Manifest-first plugins use a
plugin.jsonmanifest plus root-level directories and config files such asskills/,.mcp.json, or.app.json. - Code-first plugins are JS/TS modules that export hooks or custom tools. Skills and MCP configuration stay as separate surfaces.
- Package-based bundles use
package.jsonplus conventional directories such asskills/andextensions/.
Let's see how each platform implements that idea.
Plugin Structure by Platform
Claude Code Plugin Structure
Claude Code plugins are self-contained directories. The manifest lives in .claude-plugin/plugin.json, and the plugin's skills, hooks, MCP config, and other components stay at the plugin root.
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin manifest
├── skills/ # Collection of skills
│ ├── analyze-text/
│ │ └── SKILL.md # Skill definition
│ ├── extract-keywords/
│ │ └── SKILL.md
│ └── check-reading-level/
│ └── SKILL.md
├── agents/ # Custom agents (optional)
├── .mcp.json # MCP server configuration
├── hooks/ # Hook configuration (optional)
│ └── hooks.json
├── .lsp.json # LSP server configuration (optional)
├── settings.json # Plugin defaults (optional)
└── README.md # Documentationplugin.json (in .claude-plugin/ directory) declares the plugin:
{
"name": "text-processor-plugin",
"version": "1.0.0",
"description": "Text analysis skills powered by the text-processor MCP server",
"author": {
"name": "Your Name"
}
}Key Properties:
name— Unique plugin identifierversion— Semantic version (1.0.0, 1.1.0, etc.)description— What the plugin doesauthor— Creator information
Only plugin.json goes inside .claude-plugin/. Skills, agents, hooks, MCP config, and LSP config stay at the plugin root. Skills are namespaced when invoked:
/text-processor-plugin:analyze-textMCP servers are configured in .mcp.json at the plugin root, using the same format as project-scoped MCP config from Unit 2. The server can be local or remote:
{
"mcpServers": {
"text-processor": {
"url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
}
}
}Codex Plugin Structure
Codex plugins are also manifest-first. The manifest points to bundled skills, optional MCP config, optional app integrations, and install-surface metadata.
my-codex-plugin/
├── .codex-plugin/
│ └── plugin.json # Plugin manifest
├── skills/ # Workflow skills
│ ├── analyze-text/
│ │ └── SKILL.md
│ └── extract-keywords/
│ └── SKILL.md
├── .mcp.json # MCP servers config (optional)
├── .app.json # App integrations (optional)
├── assets/ # Icons, screenshots, logos (optional)
└── README.mdplugin.json (in .codex-plugin/ directory):
{
"name": "text-processor-plugin",
"version": "1.0.0",
"description": "Text analysis skills powered by the text-processor MCP server",
"skills": "./skills/",
"mcpServers": "./.mcp.json",
"apps": "./.app.json",
"interface": {
"displayName": "Text Processor"
}
}Key Properties:
name— Unique plugin identifierversion— Semantic versiondescription— What the plugin doesskills— Path to skills directorymcpServers— Path to MCP server config (optional)apps— Path to app integrations config (optional)interface— Install-surface metadata shown in Codex
Only plugin.json belongs in .codex-plugin/. Keep skills/, .mcp.json, .app.json, and assets/ at the plugin root.
MCP servers (optional, in .mcp.json):
{
"mcpServers": {
"text-processor": {
"url": "https://YOUR-USERNAME-text-processor-mcp.hf.space/gradio_api/mcp/"
}
}
}App integrations (optional, in .app.json):
{
"integrations": [
{
"name": "github",
"type": "oauth",
"scopes": ["repo", "user:email"]
}
]
}Marketplace installation works via two paths:
- Repo marketplace:
.agents/plugins/marketplace.json(in a repository) - Personal marketplace:
~/.agents/plugins/marketplace.json(on user's machine)
Each marketplace entry points to a plugin directory with a ./-prefixed relative source.path and install policy metadata. Installed copies are cached under ~/.codex/plugins/cache/$MARKETPLACE/$PLUGIN/$VERSION/.
TIP
Codex ships with a $skill-creator skill for scaffolding and iterating on skills inside a plugin. Invoke it with $skill-creator during a Codex session.
OpenCode Plugin Structure
OpenCode plugins are code-first. There is no plugin manifest directory like .claude-plugin/ or .codex-plugin/. A plugin is a JavaScript/TypeScript module loaded from a plugin directory or from npm.
my-opencode-project/
├── .opencode/
│ ├── plugins/
│ │ └── text-processor-plugin.ts # Local plugin module
│ └── package.json # Optional plugin dependencies
├── opencode.json # npm plugin list and agent config
└── README.mdLocal plugin module:
import type { Plugin } from "@opencode-ai/plugin"
export const TextProcessorPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
await client.app.log({
body: {
service: "text-processor-plugin",
level: "info",
message: "Text Processor plugin initialized",
},
})
return {
"tool.execute.before": async (input) => {
if (input.tool === "read") {
await client.app.log({
body: {
service: "text-processor-plugin",
level: "info",
message: "Long text reads often pair well with text-analysis tools.",
},
})
}
},
}
}The plugin factory receives \{ project, client, $, directory, worktree } — use client to talk back to the OpenCode runtime, $ for shell commands, and directory/worktree for filesystem context.
opencode.json for npm-installed plugins:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@your-org/text-processor-plugin"]
}Key Characteristics:
- Local plugins are JS/TS files in
.opencode/plugins/ - Optional npm plugins via the
pluginarray inopencode.json - Plugins export hooks or custom tools in code
- Skills and MCP servers are configured separately from plugins
- Distribution via local files or npm packages
Pi Package Structure
Pi's closest equivalent to a plugin is a Pi package. There is no hidden manifest directory. The package manifest lives in package.json, and Pi loads skills, extensions, prompts, and themes from the package root.
my-pi-package/
├── package.json # Package manifest
├── skills/ # Skill bundles
│ ├── analyze-text/
│ │ └── SKILL.md
│ └── extract-keywords/
│ └── SKILL.md
├── extensions/ # TS/JS runtime extensions (optional)
│ └── text-processor.ts
├── prompts/ # Prompt templates (optional)
├── themes/ # Themes (optional)
└── README.mdpackage.json:
{
"name": "text-processor-plugin",
"version": "1.0.0",
"keywords": ["pi-package"],
"pi": {
"skills": ["./skills"],
"extensions": ["./extensions"],
"prompts": ["./prompts"],
"themes": ["./themes"]
}
}Key Characteristics:
package.jsonis the manifest; there is no.pi-plugin/directory- Skills live in
skills/and load like any other Pi skill - Extensions add tools, commands, UI, and lifecycle handlers
- Prompt templates and themes can ship in the same package
- If you want MCP-backed tools, pair the package with
pi-mcp-adapterand a.mcp.jsonfile
Key Takeaways
Claude Code and Codex use manifest-first plugin bundles: a plugin.json manifest alongside root-level components like skills/, .mcp.json, and integration files. OpenCode uses a code-first model: plugins are JS/TS modules loaded from .opencode/plugins/ or npm, with skills and MCP configuration handled separately. Pi packages use package.json plus conventional directories such as skills/ and extensions/.
Next we'll build a plugin from scratch.