plugins
Claude Code plugin and marketplace engineering: `plugin.json` and `marketplace.json` manifests, component layout (skills, agents, hooks, MCP/LSP, monitors, themes, bin), plugin sources and hosting, te
SKILL.mdplugins/ai/skills/plugins/SKILL.md
GitHubClaude Code plugins and marketplaces
A plugin is a directory of components (skills, agents, hooks, MCP/LSP servers, monitors, themes, executables) that installs as one unit. A marketplace is a marketplace.json catalog that distributes plugins with version tracking and updates. This skill covers packaging, distributing, versioning, and operating them.
Corpus fetched 2026-08-05 against code.claude.com/docs. Many behaviors are gated on a CLI version (v2.1.x+); check claude --version before debugging a "missing" feature.
Answering rules
Always run claude plugin validate before claiming a manifest is correct — it catches required-field, JSON-syntax, path, and renames-chain errors that only surface as silent load failures at runtime. Add --strict in CI to turn warnings into failures.
Always determine the version resolution source before diagnosing "users aren't getting my update". plugin.json version beats the marketplace entry beats the git SHA — a stale plugin.json version silently masks a bumped marketplace entry.
Never put skills/, agents/, commands/, or hooks/ inside .claude-plugin/. Only plugin.json goes there; everything else lives at the plugin root. This is the single most common reason components don't appear.
Never reference paths outside the plugin directory (../shared-utils). Install copies the plugin into ~/.claude/plugins/cache — files outside it are not copied. Share within a marketplace via symlinks instead.
Always use ${CLAUDE_PLUGIN_ROOT} for bundled scripts and configs, and never write state there — it changes on every update. Persistent state goes in ${CLAUDE_PLUGIN_DATA}.
Never treat plugins as a sandbox. Plugins and marketplaces execute arbitrary code with the user's privileges; Anthropic does not verify third-party plugin contents. Install only from trusted sources, and restrict org-wide with strictKnownMarketplaces.
Plugin or standalone .claude/?
| Use | When |
|---|---|
Standalone .claude/ (skill invoked as /hello) |
Single project, personal config, experiments, you want short unnamespaced names |
Plugin (skill invoked as /plugin-name:hello) |
Sharing with a team or community, same components across projects, versioned releases, marketplace distribution |
Plugin skills are always namespaced by the plugin name — that is what prevents cross-plugin collisions. Migration mechanics (what moves where, and the agent-override trap) are in references/quickstart-and-migration.md.
Layout
my-plugin/
├── .claude-plugin/plugin.json # manifest — the ONLY thing in this directory
├── skills/<name>/SKILL.md # skills
├── commands/*.md # flat-file skills (legacy shape; prefer skills/)
├── agents/*.md # subagents
├── hooks/hooks.json # hook config
├── .mcp.json .lsp.json # MCP / LSP servers
├── monitors/monitors.json # background monitors
├── themes/*.json output-styles/*.md workflows/
├── bin/ # executables added to the Bash tool PATH
├── settings.json # defaults on enable (`agent`, `subagentStatusLine` only)
└── scripts/ LICENSE CHANGELOG.md
The manifest is optional: with no plugin.json, Claude Code auto-discovers the default directories and names the plugin after its directory. A plugin root CLAUDE.md is not loaded as context — ship instructions as a skill.
A plugin shipping exactly one skill may put SKILL.md at the plugin root; set frontmatter name explicitly, because the fallback is the install directory name (a version string that changes on every marketplace update).
Read references/components.md for every component's config format, plugin-agent frontmatter restrictions, the scoped names plugin hooks must use to target their own MCP server, and @skills-dir plugins.
plugin.json
name is the only required field. It is the namespace (/my-plugin:hello, my-plugin:code-reviewer) and the stable identity used by enabledPlugins, pluginConfigs, and /plugin install.
{
"name": "deployment-tools",
"displayName": "Deployment Tools",
"version": "2.1.0",
"description": "Deployment automation tools",
"author": { "name": "Dev Team", "email": "dev@company.com" },
"license": "MIT",
"keywords": ["deployment", "ci-cd"],
"skills": "./custom/skills/",
"dependencies": [{ "name": "secrets-vault", "version": "~2.1.0" }]
}
Rules that bite:
- Path fields:
commands,agents,workflows,outputStyles,experimental.themes,experimental.monitorsreplace their default directory.skillsadds to the defaultskills/scan — except for a marketplace entry whosesourceis the marketplace root, where listed subdirectories become the complete set. - All paths are relative to the plugin root and start with
./;skillsalso accepts".". No absolute paths, no... - Unrecognized top-level fields are ignored and reported as warnings, so a
package.json/VS Code manifest can coexist. Wrong types are errors and fail the load — exceptexperimentalandmetadata, where a non-object is only a warning. defaultEnabled: falseships a plugin installed-but-off (v2.1.154+). An existingenabledPluginsentry and a dependency requirement both override it.userConfigprompts the user for values at enable time;${user_config.KEY}substitutes into MCP/LSP configs and hook commands, but is rejected in shell-form hook commands, monitor commands, and MCPheadersHelper(shell-injection guard) — readCLAUDE_PLUGIN_OPTION_<KEY>from the environment instead.pluginConfigsis read only from user settings,--settings, and managed settings. Project and local settings entries are ignored so a cloned repo cannot inject values into hook commands.enabledPluginsis unaffected by this restriction.
Path variables, resolved inline in skill/agent content, hook and monitor commands, and MCP/LSP server fields:
| Variable | Meaning |
|---|---|
${CLAUDE_PLUGIN_ROOT} |
Plugin install directory — changes every update, treat as ephemeral |
${CLAUDE_PLUGIN_DATA} |
~/.claude/plugins/data/{id}/, survives updates, deleted on last-scope uninstall unless --keep-data |
${CLAUDE_PROJECT_DIR} |
Project root |
Read references/plugin-manifest.md for the complete field tables, userConfig schema and storage, channels, the cache/symlink rules, and the SessionStart dependency-reinstall pattern.
marketplace.json
Required: name, owner, plugins. Put it at .claude-plugin/marketplace.json in the repository root.
{
"name": "company-tools",
"owner": { "name": "DevTools Team", "email": "devtools@example.com" },
"metadata": { "pluginRoot": "./plugins" },
"plugins": [
{ "name": "code-formatter", "source": "./plugins/formatter", "version": "2.1.0" },
{ "name": "deployment-tools", "source": { "source": "github", "repo": "company/deploy-plugin" } }
]
}
A plugin entry accepts any plugin.json field plus source, category, tags, strict, relevance, defaultEnabled.
source |
Shape |
|---|---|
| Relative path | "./plugins/my-plugin" — must start with ./, no .., resolved from the marketplace root |
github |
{ "source": "github", "repo": "owner/repo", "ref?", "sha?" } |
url |
{ "source": "url", "url": "https://…", "ref?", "sha?" } |
git-subdir |
{ "source": "git-subdir", "url": …, "path": "tools/plugin", "ref?", "sha?" } — sparse clone for monorepos |
npm |
{ "source": "npm", "package": "@acme/plugin", "version?", "registry?" } |
When both ref and sha are set, sha is the effective pin. Relative paths do not resolve when the marketplace was added by direct URL to marketplace.json — only that one file is downloaded.
strict defaults to true (the plugin.json is authority; the entry supplements). strict: false makes the entry the entire definition — and a plugin.json that also declares components then becomes a load-failing conflict.
Each user may register only one marketplace per name; adding a second with the same name replaces the first. Anthropic reserves a list of marketplace names, re-checked on every load, so a marketplace whose name later becomes reserved stops loading — see references/marketplace-schema.md.
Worked example in this repo: .claude-plugin/marketplace.json uses metadata.pluginRoot: "./plugins", one relative-path entry per domain plugin, and a top-level renames map; each plugin pins its own version in plugins/<domain>/.claude-plugin/plugin.json.
Read references/marketplace-schema.md for the full field tables, shared-skills/ entry patterns, and the organization-sync source restrictions on Team/Enterprise.
Versioning and releases
Version is the cache key. If the resolved version equals the installed one, update is skipped. Resolution order, first set wins:
versioninplugin.jsonversionin the marketplace entry- Git commit SHA of the plugin source (git-backed sources)
unknown(npm sources, non-git local dirs)
| Strategy | How | Update behavior | Fits |
|---|---|---|---|
| Explicit version | "version": "2.1.0" in plugin.json |
Only on bump — pushing commits does nothing | Published plugins, stable releases |
| Commit-SHA | Omit version everywhere |
Every new commit is an update | Internal, actively developed plugins |
Never set version in both plugin.json and the marketplace entry: plugin.json wins silently, so a stale manifest masks the version you bumped in the catalog. The docs state the semver convention directly (MAJOR breaking / MINOR features / PATCH fixes) and recommend a CHANGELOG.md.
Bump on every release or existing users never receive it. This is the failure mode behind almost every "my fix didn't reach anyone" report — it applies to the skills, agents, and hooks inside the plugin too, since they ship only as part of a resolved plugin version.
Release channels: two marketplaces pointing at different refs of the same repo, assigned by group via managed extraKnownMarketplaces. Each channel must resolve to a different version string, or updates are skipped as identical.
Renaming: changing name breaks every install. Use displayName to relabel the UI. To really rename or remove, add a top-level renames entry ("old": "new" or "old": null, v2.1.193+); Claude Code rewrites enabledPlugins/pluginConfigs keys in writable scopes and shows a one-line notice. Treat renames as append-only — chains are followed hop by hop, and claude plugin validate rejects a chain that cycles or does not terminate at null or a listed plugin. Managed-settings entries are read-only, so the notice recurs each session until an admin updates them.
Dependency tags: version constraints resolve against git tags named {plugin-name}--v{version} on the marketplace repo. Create with claude plugin tag --push.
Read references/hosting-and-versioning.md for private-repo credentials, container seeding, auto-update behavior, and the full rename semantics.
Dependencies
{ "name": "deploy-kit", "version": "3.1.0",
"dependencies": ["audit-logger", { "name": "secrets-vault", "version": "~2.1.0" }] }
Bare string = any version the marketplace provides; object adds a Node-semver range and an optional marketplace. Unconstrained dependencies track latest, so an upstream release can break every install on auto-update — constrain anything you test against.
Cross-marketplace dependencies are refused unless the root marketplace lists the target in allowCrossMarketplaceDependenciesOn; trust does not chain through intermediates.
A manifest of just name + dependencies is a bundle plugin — the cleanest way to ship "the standard set" to a team, then add tools by publishing a new bundle version.
Errors (dependency-unsatisfied, range-conflict, dependency-version-unsatisfied, no-matching-tag) disable the dependent plugin until resolved; find them in /plugin Errors or claude plugin list --json. Read references/dependencies.md for range intersection, tag resolution details, and per-error resolution steps.
Team rollout
Project .claude/settings.json:
{
"extraKnownMarketplaces": {
"company-tools": { "source": { "source": "github", "repo": "your-org/claude-plugins" } }
},
"enabledPlugins": {
"code-formatter@company-tools": true,
"deployment-tools@company-tools": true
}
}
Teammates are prompted to install when they trust the folder. As of v2.1.195 an externally sourced plugin enabled only by project settings does not load until the member runs the claude plugin install command Claude Code prints.
Marketplace state is per user in ~/.claude/plugins/known_marketplaces.json, not per project; local directory sources resolve against the repo's main checkout, so worktrees share it.
Private repos: interactive commands use your normal git credential helpers, but the background auto-update pull disables credential helpers, so HTTPS private fetches fail there. Fix with an SSH remote plus ssh-agent, or a scoped global git URL rewrite. GITHUB_TOKEN alone does nothing.
Containers/CI: prebuild ~/.claude/plugins and point CLAUDE_CODE_PLUGIN_SEED_DIR at it — read-only, precedence over user config, no runtime cloning.
Enterprise lockdown
Managed-settings-only controls:
| Setting | Effect |
|---|---|
strictKnownMarketplaces |
Allowlist of addable marketplace sources. [] = total lockdown incl. the official marketplace. Exact matching; prefer hostPattern/pathPattern regex entries over literal URLs |
blockedMarketplaces |
Denylist, checked before any download |
disableSideloadFlags |
Rejects --plugin-dir, --plugin-url, --agents, --mcp-config (v2.1.193+) |
pluginSuggestionMarketplaces |
Which marketplaces may surface "suggested for this directory" plugins |
strictKnownMarketplaces restricts what can be added; it does not register anything. Pair it with extraKnownMarketplaces in the same managed settings, or users on locked-down machines end up with no marketplace at all. Enforcement runs on add and on every install/update/refresh, so a pre-existing marketplace that no longer matches stops updating.
Read references/team-and-enterprise.md for scope precedence, managed-settings delivery mechanisms, and the exact allowlist matching rules.
Develop, test, validate
claude plugin init my-tool --with skills hooks # scaffold into ~/.claude/skills/ as my-tool@skills-dir
claude --plugin-dir ./my-plugin # session-only; beats an installed plugin of the same name
claude --plugin-url https://ci/artifact.zip # session-only from a hosted .zip
claude plugin validate ./my-plugin --strict # CI gate
claude plugin details my-plugin # component inventory + projected token cost
/reload-plugins picks up changes without a restart (skills reload immediately from SKILL.md edits; other components need the reload). Its skill count only counts commands/ directories, so 0 skills after reloading a skills/-based skill is a display artifact, not a failure.
Validate before publishing anywhere — the community-marketplace review pipeline runs the same check plus safety screening.
Practice recommendations (advice, not documented behavior): keep version only in plugin.json so there is one source of truth; run claude plugin validate . --strict on every PR touching a manifest; pin sha rather than ref for third-party sources in regulated environments; keep a CHANGELOG.md entry per bump so /plugin users can tell what changed.
Debugging
Start with claude --debug (shows plugin load, manifest errors, component registration, MCP init) and the /plugin Errors tab.
| Symptom | Cause | Fix |
|---|---|---|
| Plugin doesn't load | Invalid plugin.json |
claude plugin validate ./my-plugin |
| Skills missing | Components inside .claude-plugin/ |
Move to plugin root |
| Hooks never fire | Script not executable, or wrong event case | chmod +x, PostToolUse not postToolUse |
| MCP server fails | Path not using ${CLAUDE_PLUGIN_ROOT} |
Use the variable everywhere |
| Path errors | Absolute path, or missing ./ |
Make relative, prefix ./ |
conflicting manifests |
strict: false plus a component-declaring plugin.json |
Drop one of the two definitions |
| Whole plugin gone after adding hooks | Malformed hooks/hooks.json |
Fix JSON — it blocks the entire plugin |
Old name reports plugin-not-found |
Client below v2.1.193 ignores renames |
Upgrade, or reinstall under the new name |
Read references/cli-and-debugging.md for every claude plugin subcommand and flag, the /plugin UI behavior, and the full validation error/warning catalog.
Unverified
The exact JSON shape of enabledPlugins and pluginConfigs when an administrator writes them directly into managed settings (as opposed to Claude Code writing them via /plugin install or the userConfig prompt) could not be confirmed against a verbatim source page in this corpus. The object-map forms shown above are confirmed for user/project settings and for values Claude Code writes itself. Verify managed-settings shapes against the live settings reference before deploying policy.
Reference files
references/plugin-manifest.md— everyplugin.jsonfield, path-field semantics,userConfig,channels, env vars, cache and symlink rulesreferences/components.md— skills, agents, hooks, MCP, LSP, monitors, themes,bin/, pluginsettings.json,@skills-dirpluginsreferences/marketplace-schema.md—marketplace.jsonschema, plugin entries, sources, strict mode, reserved names, org syncreferences/hosting-and-versioning.md— hosting, private repos, seeding, version resolution, release channels,renamesreferences/dependencies.md—dependenciesfield, semver ranges, cross-marketplace allowlist, tagging, error catalogreferences/cli-and-debugging.md—claude pluginCLI reference,/pluginUI, auto-update, debugging, validation catalogreferences/team-and-enterprise.md— install scopes, settings keys,strictKnownMarketplacesand friendsreferences/quickstart-and-migration.md— minimal plugin end-to-end, migrating.claude/, official marketplace catalog
Diagnostic script
scripts/lint-plugin-manifests.py— read-only lint of aplugin.jsonand/or.claude-plugin/marketplace.jsonagainst documented rules (required fields, component-dir placement, path shapes, dual-version pinning, duplicate/reserved names,renameschain termination, cross-marketplace dependency allowlist). Complementsclaude plugin validate; it does not replace it.
Sources
- https://code.claude.com/docs/en/plugins
- https://code.claude.com/docs/en/plugins-reference
- https://code.claude.com/docs/en/plugin-marketplaces
- https://code.claude.com/docs/en/plugin-dependencies
- https://code.claude.com/docs/en/discover-plugins
- https://code.claude.com/docs/en/settings
Fetched: 2026-08-05