Building and distributing a plugin or a private marketplace
A coding-agent plugin is a directory with a manifest (.claude-plugin/plugin.json, .codex-plugin/plugin.json or .cursor-plugin/plugin.json) plus skills, hooks and MCP configuration, published through a marketplace: a git repository with a catalogue file. Skills and MCP servers carry across Claude Code, Codex and Cursor; hooks do not, so each tool needs its own hook file and its own test.
Your team has a migration skill in one repository, a PreToolUse hook in someone’s ~/.claude/settings.json, and a staging-database MCP server that three people configured three different ways. A new hire gets none of it, and a fix to the hook reaches nobody. This page is for the developer who packages that setup once and the tech lead who owns the private marketplace it ships from.
What you get from packaging your team’s agent setup as a plugin
Section titled “What you get from packaging your team’s agent setup as a plugin”- A working plugin,
acme-db-guard, that bundles a skill, a blocking hook and a read-only MCP server, with every file shown - A verification routine that proves the plugin works before anyone installs it:
validate --strict, a hook test,claude plugin evalagainst a no-plugin baseline, and a context-cost reading - A private marketplace that Claude Code and Codex both install from, plus the managed settings that pin it for the whole team
- A release rule for versions, a CI job that enforces it, and a table of what does and does not port between tools
Everything marked as run below was run on 2026-09-26 against Claude Code 2.1.283 (the scaffold step re-checked on 2.1.286) and codex-cli 0.157.1 in a scratch home directory. Cursor details come from Cursor’s first-party cursor/plugins repository; cursor.com was not reachable, so no Cursor step was run.
Should this be a plugin, a skill or an MCP server?
Section titled “Should this be a plugin, a skill or an MCP server?”Build a plugin only when you need to ship parts that depend on each other under one version. A lone procedure is a skill; a lone connection is an MCP server.
| Your situation | Build |
|---|---|
| One reusable procedure, used across several agents | A skill, installed with npx skills |
| One live connection to an internal API | An MCP server, added per project |
| A procedure that only works with a hook or server beside it | A plugin |
| The same setup must arrive on every laptop, and a fix must reach everyone | A plugin in a team marketplace |
| Vendor-specific behaviour, such as a PR bot | None of these: configure the vendor product |
The plugins overview covers installing other people’s plugins. This page covers building your own.
What goes into a plugin in Claude Code, Codex and Cursor?
Section titled “What goes into a plugin in Claude Code, Codex and Cursor?”The component folders are the same idea in all three tools. The manifest location and the hook format differ.
acme-plugins/ # the marketplace repository├── .claude-plugin/marketplace.json # catalogue for Claude Code (Codex also reads it)├── .agents/plugins/marketplace.json # catalogue for Codex (optional)└── plugins/acme-db-guard/ ├── .claude-plugin/plugin.json # Claude Code manifest ├── .codex-plugin/plugin.json # Codex manifest (optional) ├── skills/new-migration/SKILL.md # Agent Skills format: portable ├── hooks/hooks.json # Claude Code hook format: not portable ├── scripts/guard-applied-migrations.sh └── .mcp.json # MCP servers| Part | Claude Code | Codex | Cursor |
|---|---|---|---|
| Manifest | .claude-plugin/plugin.json | .codex-plugin/plugin.json | .cursor-plugin/plugin.json |
| Marketplace file | .claude-plugin/marketplace.json | .agents/plugins/marketplace.json, or the Claude file | .cursor-plugin/marketplace.json |
| Skills | skills/<name>/SKILL.md, found automatically | skills/; a "skills" path in the manifest adds to it | "skills": "./skills/" in the manifest |
| Hooks | hooks/hooks.json, found automatically | scaffolded by the plugin-creator skill; format not verified here | "hooks": "./hooks/hooks.json", a different schema |
| MCP servers | .mcp.json | "mcpServers": "./.mcp.json" | mcp.json (per cursor/plugins) |
| Plugin root variable | ${CLAUDE_PLUGIN_ROOT} | not verified | ${CURSOR_PLUGIN_ROOT} (per cursor/plugins hooks.json) |
Build the acme-db-guard plugin step by step
Section titled “Build the acme-db-guard plugin step by step”The example guards database migrations. The skill tells the agent how to write a new migration, the hook blocks edits to migrations git already tracks, and the MCP server lets the agent inspect staging without write access.
-
Scaffold the plugin. Start from each tool’s scaffolder, then replace the placeholder files with the ones below.
Terminal window claude plugin init acme-db-guard --with skills hooks mcp --description "Migration guard"This writes a plugin to
~/.claude/skills/acme-db-guard/that loads next session asacme-db-guard@skills-dir. With--with skills hooks mcpit also writes example files: delete the generatedSKILL.mdat the plugin root,skills/example/andhooks-handlers/(aSessionStarthandler) before adding the files below, or you ship an example skill and a hook you never wrote (checked on 2.1.286). Move the folder into your marketplace repository when it works. For a guided build,plugin-dev@claude-plugins-officialadds a/plugin-dev:create-pluginskill and a validator subagent; it costs about 2,349 always-on tokens, so enable it only while you author.Ask Codex to use its built-in
plugin-creatorskill. It runsscripts/create_basic_plugin.py <name>with flags such as--with-skills --with-hooks --with-mcp --with-marketplace, writes.codex-plugin/plugin.json, and checks the result withscripts/validate_plugin.py.Type
/add-plugin create-pluginin Agent chat, then run/create-pluginwith a name, a purpose and the component types. Itsreview-plugin-submissionskill checks the result. Both come from thecursor/pluginsREADME and were not run here. -
Write the manifest.
namebecomes the namespace for every skill and command, so/acme-db-guard:new-migrationis how a user calls the skill. Never rename a published plugin.plugins/acme-db-guard/.claude-plugin/plugin.json {"name": "acme-db-guard","version": "0.1.0","description": "Migration skill, schema guard hook and read-only Postgres MCP","author": { "name": "Acme Platform Team" }} -
Add the skill. The
descriptiondecides when the agent loads the skill, so name the trigger phrases.plugins/acme-db-guard/skills/new-migration/SKILL.md ---name: new-migrationdescription: Use when the user asks to change the database schema, add a column, add an index or write a migration. Writes a new numbered file in db/migrations/ and never edits an applied one.---# New migration1. List db/migrations/ and pick the next number.2. Write the forward migration only; create indexes with CREATE INDEX CONCURRENTLY in a migration of its own, because it cannot run inside a transaction.3. Run the migration against a scratch database and the test suite, and paste both results. -
Add the hook. The skill asks; the hook enforces. Exit code
2blocks the tool call and sends the message on stderr back to the agent.plugins/acme-db-guard/hooks/hooks.json {"hooks": {"PreToolUse": [{"matcher": "Edit|Write","hooks": [{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/guard-applied-migrations.sh\"" }]}]}}plugins/acme-db-guard/scripts/guard-applied-migrations.sh #!/usr/bin/env bashcommand -v jq >/dev/null && command -v git >/dev/null || { echo "acme-db-guard: jq and git are required" >&2; exit 1; }path=$(jq -r '.tool_input.file_path // empty')case "$path" in*/db/migrations/*)if git ls-files --error-unmatch "$path" >/dev/null 2>&1; thenecho "Blocked: $path is already committed. Write a new migration instead." >&2exit 2fi ;;esacexit 0Mark the script executable (
chmod +x) before you commit it. The script needsjqandgitonPATH; a plugin cannot install them, so the first line exits 1 with a message when either is missing. Exit 1 surfaces the error without blocking every tool call. -
Add the MCP server. Reference secrets as
${VARIABLE}so no credential enters the repository. Postgres MCP Pro is the PyPI packagepostgres-mcp, run throughuvx; the npm package of the same name is unrelated. Its last PyPI release, 0.3.0, dates from 2025-05-16, so pin the version (postgres-mcp==0.3.0) and swap in another database server if it stays unmaintained.plugins/acme-db-guard/.mcp.json {"mcpServers": {"staging-db": {"command": "uvx","args": ["postgres-mcp==0.3.0", "--access-mode=restricted"],"env": { "DATABASE_URI": "${ACME_STAGING_DATABASE_URI}" }}}} -
Load it for one session.
claude --plugin-dir ./plugins/acme-db-guardloads the plugin without installing it. After an edit,/reload-pluginspicks up the change without a restart.
Prove the plugin works before anyone installs it
Section titled “Prove the plugin works before anyone installs it”A plugin runs on every teammate’s machine with their credentials, so “it loaded on mine” is not evidence. Run these four checks; together they replace reading the plugin line by line.
-
Validate strictly.
--strictturns warnings into a non-zero exit, so it catches unrecognised fields and missing metadata that the runtime tolerates.Terminal window claude plugin validate --strict ./acme-pluginsclaude plugin validate --strict ./acme-plugins/plugins/acme-db-guardValidate the marketplace and each plugin separately. Validation of the marketplace does not catch a
sourcedirectory that does not exist; only the install fails. -
Test the hook without an agent. Feed the script the JSON a
PreToolUseevent sends and check the exit codes:Terminal window echo '{"tool_input":{"file_path":"'"$PWD"'/db/migrations/001_init.sql"}}' \| plugins/acme-db-guard/scripts/guard-applied-migrations.sh; echo "exit=$?"# Blocked: …/db/migrations/001_init.sql is already committed. Write a new migration instead.# exit=2A new, untracked file must return
exit=0. The command above works only in a repository wheredb/migrations/001_init.sqlis committed, and the CI job below runs in the marketplace repository, which has no such file. So the plugin’s own test script builds that fixture in a temporary repository and asserts both cases. Make it executable; the CI job runs it on every pull request.plugins/acme-db-guard/tests/hook.sh #!/usr/bin/env bashset -uguard="$(cd "$(dirname "$0")/.." && pwd)/scripts/guard-applied-migrations.sh"tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXITgit -C "$tmp" init -qmkdir -p "$tmp/db/migrations"; echo x > "$tmp/db/migrations/001_init.sql"git -C "$tmp" add -Agit -C "$tmp" -c user.name=t -c user.email=t@t commit -qm initcd "$tmp"check() { # $1 = file, $2 = expected exit codeecho '{"tool_input":{"file_path":"'"$tmp/$1"'"}}' | "$guard" 2>/dev/nullgot=$?; [ "$got" -eq "$2" ] || { echo "FAIL $1: exit $got, want $2"; exit 1; }}check db/migrations/001_init.sql 2check db/migrations/002_add_index.sql 0echo "hook tests passed"Break the guard (change its
exit 2toexit 0) once and confirm the script printsFAILand exits 1; a test that cannot go red proves nothing. -
Measure what it costs. Install from the local marketplace and read the inventory:
Terminal window claude plugin marketplace add ./acme-pluginsclaude plugin install acme-db-guard@acme-pluginsclaude plugin details acme-db-guardComponent inventorySkills (1) new-migrationHooks (1) PreToolUse (harness-only — no model context cost)MCP servers (1) staging-db (tool schemas resolved at runtime; not counted)Projected token costAlways-on: ~71 tok added to every session…The output above is trimmed; the full one also lists empty component types and a per-component table. Expect about 70 tokens (2.1.283); a figure a few tokens off on another version is normal. MCP tool schemas are not in that figure, so also check
/contextin a session with the server connected. -
Score it against a baseline.
claude plugin evalruns cases from the plugin’sevals/directory with and without the plugin and reports the difference.claude plugin eval init --bare new-migration-numberingcreatesevals/new-migration-numbering/prompt.md(the task,max_turns,allowed_tools) andgraders/criteria.md(an LLM-graded definition of success).Terminal window claude plugin eval ./acme-plugins/plugins/acme-db-guard --runs 3 --threshold 0.8 --no-publishThe command exits
1when any case scores below the threshold. Eval runs the plugin on your machine as you; pass--trust-pluginonly for plugins you wrote.Eval scores the skill. Under the default
--mocks recordit does not startstaging-dbunless you record a mock or pass--allow-real-servers, and the hook fires only if a case can call Edit or Write (--allow-tools Edit Write). The piped-JSON test in step 2 remains the hook’s proof (checked on 2.1.283).
Who signs off: the plugin’s owner (a named person in CODEOWNERS for plugins/acme-db-guard/) approves a release only when all four checks pass in the pull request. Reviewers read the eval deltas and the cost figure, not every line of the skill.
Publish a private team marketplace
Section titled “Publish a private team marketplace”The marketplace is the catalogue file at the repository root. Its name is what users type after @, so pick it once.
{ "name": "acme-plugins", "description": "Acme platform team plugins", "owner": { "name": "Acme Platform Team" }, "plugins": [ { "name": "acme-db-guard", "source": "./plugins/acme-db-guard", "description": "Migration skill, schema guard hook and read-only Postgres MCP" } ]}validate --strict fails without the top-level description. Push the repository to a private GitHub or GitLab repository. Teammates then install it per tool:
claude plugin marketplace add acme/acme-pluginsclaude plugin install acme-db-guard@acme-pluginsClaude Code clones with the machine’s git credentials and never prompts. For HTTPS on GitHub, run gh auth login and then gh auth setup-git; a GITHUB_TOKEN in the environment does nothing without a credential helper. Set CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 to skip the SSH attempt.
To pin the plugin to one repository, run both commands with --scope project. That writes .claude/settings.json:
{ "extraKnownMarketplaces": { "acme-plugins": { "source": { "source": "github", "repo": "acme/acme-plugins" } } }, "enabledPlugins": { "acme-db-guard@acme-plugins": true }}Commit that file. The marketplace registers after each teammate trusts the folder, and a plugin with an external source still needs one claude plugin install … --scope project per machine.
codex plugin marketplace add acme/acme-plugins --ref maincodex plugin add acme-db-guard@acme-pluginscodex plugin list# acme-db-guard@acme-plugins installed, enabled 0.1.0Codex reads .claude-plugin/marketplace.json when it is the only catalogue. When the repository also has .agents/plugins/marketplace.json, Codex uses that file instead (observed on 0.157.1). The Codex catalogue needs a policy and a category per entry:
{ "name": "acme-plugins", "interface": { "displayName": "Acme platform team" }, "plugins": [ { "name": "acme-db-guard", "source": { "source": "local", "path": "./plugins/acme-db-guard" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Developer Tools" } ]}Refresh with codex plugin marketplace upgrade acme-plugins, not update.
Cursor needs .cursor-plugin/marketplace.json at the repository root, in the same shape as cursor/plugins uses (name, owner, plugins[] with name, source and description). An admin imports the repository as a Team Marketplace under Dashboard → Settings → Plugins, picks access groups, and marks each plugin Required or Optional; members install with /add-plugin. The import steps come from secondary sources (cursor.com was unreachable on 2026-09-26), so confirm them in your dashboard.
To limit the marketplaces a team can add in Claude Code to an allowlist that includes yours, a platform admin sets managed settings (server-managed, MDM or managed-settings.json):
{ "strictKnownMarketplaces": [ { "source": "github", "repo": "anthropics/claude-plugins-official" }, { "source": "github", "repo": "acme/*" }, { "source": "skills-dir" } ], "extraKnownMarketplaces": { "acme-plugins": { "source": { "source": "github", "repo": "acme/acme-plugins" }, "autoUpdate": true } }, "enabledPlugins": { "acme-db-guard@acme-plugins": true }}Keep the { "source": "skills-dir" } entry: without it the policy also blocks the claude plugin init scaffolds that plugin authors test with. Third-party marketplaces do not auto-update until a user or an admin turns it on, which is why autoUpdate is set here. One policy for every coding agent covers the Codex and Cursor equivalents. To run the marketplace over time (private-repo authentication, rollout, retiring a plugin), see Publishing a private plugin marketplace for your team.
Version and release a plugin so fixes reach everyone
Section titled “Version and release a plugin so fixes reach everyone”A pushed commit does not update anyone while plugin.json still carries the old version. Choose one rule and write it in the marketplace README:
- Semantic versions. Bump
versioninplugin.jsonon every release. Do not also put a version in the marketplace entry; one source avoids a mismatch. - Commit tracking. Omit
versioneverywhere, and users track the latest commit.
Tag each release so a teammate can pin or roll back:
# after bumping "version" to 0.1.1 in plugin.json and committingclaude plugin tag --dry-run ./plugins/acme-db-guard# … Dry run — would create tag acme-db-guard--v0.1.1 at HEAD in …claude plugin tag --push ./plugins/acme-db-guardclaude plugin tag checks that plugin.json and the marketplace entry agree before it creates the <name>--v<version> tag. Teammates then pick up the release:
claude plugin marketplace update acme-pluginsclaude plugin update acme-db-guard@acme-plugins# Plugin "acme-db-guard" updated from 0.1.0 to 0.1.1 for scope user. Restart to apply changes.Codex keeps each installed version in its own cache directory (plugins/cache/acme-plugins/acme-db-guard/0.1.0/ on 0.157.1). Run codex plugin marketplace upgrade acme-plugins, then codex plugin add acme-db-guard@acme-plugins again. During local development, OpenAI’s plugin-creator skill changes a +codex.<token> suffix on the version rather than the version number itself.
Enforce the rule in CI. This job holds no secrets and needs none, because validation makes no model call:
name: plugin-marketplaceon: pull_request: paths: ['plugins/**', '.claude-plugin/**', '.agents/**']permissions: contents: readjobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: actions/setup-node@v7 with: node-version: 22 - run: npm install -g @anthropic-ai/claude-code@2.1.283 - run: claude plugin validate --strict . - run: for p in plugins/*/; do claude plugin validate --strict "$p" || exit 1; done - run: for s in plugins/*/scripts/*.sh; do [ -e "$s" ] || continue; [ -x "$s" ] || { echo "$s not executable"; exit 1; }; done - run: for t in plugins/*/tests/*.sh; do [ -e "$t" ] || continue; [ -x "$t" ] || { echo "$t not executable"; exit 1; }; "$t" || exit 1; done - name: Claude and Codex manifests carry the same version run: | for p in plugins/*/; do [ -f "$p/.codex-plugin/plugin.json" ] || continue a=$(jq -r .version "$p/.claude-plugin/plugin.json") b=$(jq -r .version "$p/.codex-plugin/plugin.json" | cut -d+ -f1) [ "$a" = "$b" ] || { echo "$p: $a vs $b"; exit 1; } doneThe version-parity step is our suggestion: claude plugin tag checks only the Claude files, so nothing else notices a Codex manifest left behind. Run claude plugin eval locally or in a separate job on the default branch, because it calls a model and needs a key.
How portable is one plugin across Claude Code, Codex and Cursor?
Section titled “How portable is one plugin across Claude Code, Codex and Cursor?”Codex 0.157.1 installed acme-db-guard straight from the Claude-format marketplace, with no Codex manifest, and copied every folder, hooks included, into its cache. Copying is not activation. This table separates the two.
| Component | Claude Code | Codex | Cursor | Portable? |
|---|---|---|---|---|
Skills (SKILL.md) | Yes | Yes | Yes, via "skills" in the manifest | Yes: Agent Skills is an open standard |
| MCP servers | .mcp.json | "mcpServers" pointing at the same .mcp.json | mcp.json | Mostly: same server, different file or key |
| Hooks | hooks/hooks.json, PreToolUse and other Claude events | not verified whether Claude-format hooks activate; Codex asks you to trust a plugin’s hooks in /hooks before they run (0.157.1) | "version": 1 schema with camelCase events such as stop and afterAgentResponse | No: write and test one hook file per tool |
| Slash commands, subagents | Yes | not verified from a Claude-format plugin | Cursor has its own commands/ and agents/ folders | No |
| Namespacing | /<plugin>:<skill> | not verified | not verified | Test in each tool |
The practical rule: put the procedure in the skill and the connection in the MCP server, because those two travel. Keep hooks thin, one small script that each tool’s hook file calls, so the logic lives once even though the wiring does not. In Cursor’s "version": 1 hooks file, call the same guard as "${CURSOR_PLUGIN_ROOT}/scripts/guard-applied-migrations.sh", not as a path relative to the working directory.
When a plugin release breaks, and how to recover
Section titled “When a plugin release breaks, and how to recover”- Teammates still run the old hook after your fix. The version did not change, or auto-update is off. Bump
version, tag, and ask them to runclaude plugin marketplace update acme-pluginsandclaude plugin update acme-db-guard@acme-plugins, then restart. - The hook blocks every edit. A hook that exits
2on every call locks the agent out. Disable the plugin at once withclaude plugin disable acme-db-guard@acme-plugins, reproduce with the piped-JSON test above, and add that input as a test case before you re-release. - The hook does nothing on one laptop.
jqorgitis missing, or the script lost its executable bit in a zip. Checkls -lon the installed copy and make the script fail loudly when a tool is missing, as the script above does. - Install fails with
Source path does not exist. A plugin was renamed or moved without updatingsource. Fix the entry, and validate each plugin directory in CI, not only the marketplace. - The MCP server starts with an empty connection string. The teammate never exported
ACME_STAGING_DATABASE_URI. Document required variables in the skill and in the marketplace README; never fall back to a literal credential. - The plugin works in Claude Code and silently does nothing in Codex. Codex used the other catalogue, or the component is not one Codex activates. Run
codex plugin listto see whichmarketplace.jsonit read, and test each component in a real Codex session.
How popular plugin building is, and what it costs in context
Section titled “How popular plugin building is, and what it costs in context”As of 2026-09-26, Anthropic’s plugin-dev authoring plugin had 67,663 installs on the claude.com/plugins directory. The official catalogues held 314 entries in claude-plugins-official, 65 in Codex’s openai-curated and 94 in Cursor’s cursor-plugins (counted from each repository’s marketplace file). The acme-db-guard example adds about 71 always-on tokens per session, against about 2,349 for plugin-dev itself, so keep authoring kits disabled outside authoring sessions.