What an extension package is
An extension is a directory with a package.json manifest, one or more TypeScript factory modules, and whatever capability folders you want to ship alongside them. The runtime piece — what pi.registerTool, pi.registerCommand, and pi.on actually run — is the factory module the manifest points at. Everything else in the directory (skills, hooks, custom tools, prompt templates, an mcp.json, themes) is picked up by omp’s existing discovery surfaces the moment the package is on the load path. The manifest is the only file that has to exist; the rest is convention.
This page is about packaging. For the individual surfaces, see Skills, Hooks, Custom tools, and MCP.
The manifest
omp reads one field from package.json: omp.extensions. It is an array of entry paths, each resolved relative to the package root. Each path is a .ts or .js module that default-exports a factory taking ExtensionAPI.
{
"name": "my-extension",
"version": "0.1.0",
"omp": {
"extensions": ["./src/main.ts"]
}
}A package may declare several entries — useful when one bundle wants to keep, say, the safety hook separate from the productivity tools:
{
"omp": {
"extensions": ["./src/safety.ts", "./src/tools.ts"]
}
}The legacy key pi.extensions is still accepted with the same shape; new packages should use omp.extensions.
Directory layout
omp discovers capabilities by directory name, not by manifest field. If you ship the conventional folders next to your factory, they are loaded as if the user had placed them under ~/.omp/agent/ themselves.
my-extension/
package.json ← omp.extensions manifest
src/
main.ts ← extension factory (registers tools, commands, events)
skills/
my-skill/
SKILL.md ← on-demand playbook
hooks/
pre/
block-rm.ts ← legacy HookAPI module
tools/
my-tool/
index.ts ← custom tool factory
prompts/
review.md ← prompt template
mcp.json ← additional MCP servers
themes/
midnight.json ← theme
README.mdSub-discovery rules match the standalone surfaces: skills are one directory deep under skills/, hooks under hooks/pre/ and hooks/post/, custom tools at tools/<name>/index.ts. The factory in src/main.ts runs in addition to (not instead of) those.
A complete example
A minimal package that registers one tool, one slash command, and ships a skill:
{
"name": "@acme/notes",
"version": "1.0.0",
"description": "Notes search tool plus a writing-style skill",
"omp": {
"extensions": ["./src/main.ts"]
}
}// src/main.ts
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent"
export default function notes(pi: ExtensionAPI) {
const { z } = pi.zod
pi.registerCommand("notes", {})
}description: "Open today's note",
handler: async (_args, ctx) => ctx.ui.notify("Opened notes", "info"),
});
pi.registerTool({
name: “search_notes”,
label: “Search Notes”,
description: “Full-text search through project notes”,
parameters: z.object({ query: z.string() }),
async execute(_id, params) {
return {
content: [{ type: “text”, text: Searched: ${params.query} }],
details: { query: params.query },
};
},
});
}
`skills/notes-style/SKILL.md` and the rest of the conventional folders need no wiring — drop them in and omp finds them when the package is on the load path.
## Test it locally
Three ways to load a package while developing it. All three are equivalent from the runtime's point of view; pick whichever matches how you want to iterate.
- **Point settings at the directory.** Add the absolute path to `extensions` in `~/.omp/agent/config.yml`:
```yaml
extensions:
- /path/to/my-extension
-
One-shot via the CLI.
omp --extension ./my-extensionloads the package for a single session.--hookis the same flag under a different name. -
Install as a plugin.
omp install ./my-extension(or-l ./my-extensionfor project scope) symlinks the directory into the plugin set and watches it for changes — the right choice when you want to dogfood it alongside your real toolset. See Plugins.
Confirm what loaded with omp -p '/extensions'. Run with --log-level debug to see the per-surface load lines.
Ship it through a marketplace
A marketplace is a Git repository with a .claude-plugin/marketplace.json catalog listing one or more packages. The catalog is where versioning and metadata live — name, version, author, category, tags, homepage — not the package itself. A minimal entry pointing at a sibling directory in the same repo:
{
"name": "acme-plugins",
"owner": { "name": "Acme Corp" },
"plugins": [
{
"name": "notes",
"version": "1.0.0",
"category": "productivity",
"source": "./plugins/notes"
}
]
}Push the repo; users add it with omp marketplace add owner/repo and install with omp install notes@acme-plugins. Pin versions by tagging the marketplace repo and updating the catalog’s version field — installs respect the pin. See Marketplace for the full catalog schema and source types (Git URL, GitHub shorthand, git-subdir, npm).
Related
-
Marketplace — catalog schema and publishing workflow.
-
Plugins — install, scope, and update mechanics.
-
Skills — playbooks bundled under
skills/. -
Hooks — event interceptors bundled under
hooks/.\n