Plugin manifest reference
Every plugin.json field with its type and behaviour, component path rules, userConfig and channels schemas, path variables and the default layout.
plugin.json lives at .claude-plugin/plugin.json inside a plugin and describes it: metadata, where components live if not in their default folders, inline component definitions, and any values the user should be prompted for. This page is the field-by-field reference. If you are building your first plugin, Create a plugin is a gentler start, and Plugin components explains what each component does at runtime.
Do you need a manifest at all?
No. Without one, Claude Code loads whatever it finds in the standard layout, and the plugin's name comes from its marketplace entry (or from the folder name when loaded with --plugin-dir).
Add a manifest when you want metadata, need a component outside its default folder, want userConfig prompts, or want to define components inline. The manifest goes inside .claude-plugin/; everything else (skills/, hooks/, commands/ and so on) sits at the plugin root, never inside .claude-plugin/.
A fuller example
This manifest is for a plugin I use to manage releases of client sites. It exercises most of the fields on this page:
{
"$schema": "https://example.com/schemas/claude-plugin.json",
"name": "site-release",
"displayName": "Site Release",
"version": "0.9.0",
"description": "Release checklist skill, changelog agent, Lighthouse monitor and a CMS MCP server",
"author": { "name": "Cameron Shields", "url": "https://cameronshields.co.uk" },
"homepage": "https://cameronshields.co.uk/tools/site-release",
"repository": "https://github.com/cshields/site-release",
"license": "MIT",
"keywords": ["release", "nextjs", "lighthouse"],
"defaultEnabled": true,
"dependencies": ["copy-check"],
"metadata": { "internalCode": "SR-01" },
"skills": ["./release-skills/"],
"commands": {
"preflight": { "source": "./commands/preflight.md", "argumentHint": "[branch]" },
"whatsnew": { "content": "Summarise merged pull requests since the last tag.", "description": "Draft release notes" }
},
"agents": ["./agents/changelog-writer.md"],
"hooks": "./config/release-hooks.json",
"mcpServers": {
"cms": {
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/mcp/cms-server.js"],
"env": { "CMS_URL": "${user_config.cms_url}" }
}
},
"outputStyles": "./styles/",
"experimental": {
"monitors": "./config/monitors.json"
},
"userConfig": {
"cms_url": {
"type": "string",
"title": "CMS base URL",
"description": "Root URL of the headless CMS for this client"
},
"cms_token": {
"type": "string",
"title": "CMS API token",
"description": "Personal access token for the CMS",
"sensitive": true
}
}
}
Unknown fields
Two different behaviours apply:
- Unknown top-level keys are stripped and the plugin still loads. The validator warns about each one.
- Strict objects (
userConfigoptions,channelsentries,lspServersconfigs andmonitorsentries) reject unknown keys. One stray key there is an error and the plugin does not load.
Validating
claude plugin validate is the authority on whether a manifest is acceptable:
claude plugin validate ./site-release
| Result | Meaning |
|---|---|
Validation passed | Loads cleanly |
Validation passed with warnings | Loads, but something wants fixing: a stripped unknown field, a non-kebab-case name, or a missing version, description or author. Add --strict in CI to fail on warnings |
Validation failed | A type mismatch, a missing or escaping path, or an unknown key in a strict object. Loading the plugin hits the same problem |
From v2.1.281 the validator also checks every MCP server the plugin declares (in .mcp.json, a JSON file named by mcpServers, or inline). Errors: an entry that would be dropped at load, a ${user_config.KEY} reference to an undeclared option, or a remote url that is not a valid absolute URL. Warnings: http:// or ws:// to a non-loopback host, and header values that look like hard-coded credentials.
Top-level fields
Only name is required. "Path" means a string relative to the plugin root, starting ./.
Identity and metadata
| Field | Type | Notes |
|---|---|---|
$schema | string | JSON Schema URL for editor completion. Ignored at load |
name | string | Required. Kebab-case identifier; every component is namespaced under it. See naming rules |
displayName | string | Friendly label shown in the UI instead of name. Can contain spaces and capitals. Not used for lookup. A marketplace entry's displayName overrides it |
version | string | Free-form (not checked as semver). Setting it holds users on that version until you change it. Not honoured for command sources, claude.ai-hosted marketplaces, or in-place local plugins. See How plugins load |
description | string | One-line summary |
author | object | name (required), optional email and url |
homepage | string | Documentation URL. Must parse as a URL or the plugin fails to load |
repository | string | Source URL. Not validated |
license | string | SPDX identifier, e.g. MIT |
keywords | string array | Discovery tags |
metadata | object | Anything you like (catalogue codes, entitlements). Claude Code never reads it. Needs v2.1.222 or later |
Directory listing fields
icon, documentationUrl, supportUrl, privacyPolicyUrl and termsOfServiceUrl are read by Anthropic's plugin directory when you submit (see Publish a plugin); Claude Code ignores them. Set icon to an image path inside the plugin such as ./icon.png, and the four URLs to https:// addresses. Put them only in plugin.json: in a marketplace entry they are reported as unknown fields. Versions before v2.1.281 warn about them even in plugin.json, which makes --strict runs fail on those versions.
Behaviour
| Field | Type | Notes |
|---|---|---|
defaultEnabled | boolean | Whether the plugin starts on when the user has no enabledPlugins entry. Default true. A plugin required by another enabled plugin starts on regardless. A marketplace entry's value overrides this. Once a user's entry exists it survives updates, so changing this later does not affect existing users |
dependencies | array | Plugins that must also be enabled. Each item is "name", "name@marketplace" or { "name", "marketplace", "version" }. Bare names resolve in this plugin's own marketplace. See Plugin dependencies |
settings | object | Settings applied while enabled. Only agent and subagentStatusLine take effect; others are dropped. A root-level settings.json overrides this key |
userConfig | object | Values to prompt for. See User configuration |
channels | array | Message channels bound to the plugin's MCP servers. See Channels |
types | path | A .d.ts file declaring the $.state values and $ nouns of a mod. See Mods reference |
Component locations
| Field | Accepts | Relationship to default |
|---|---|---|
skills | path or array of paths (directories) | Adds to skills/. Each path is either a folder of <name>/SKILL.md folders or one folder containing SKILL.md. "." means the plugin root |
commands | path, array of paths, or object map | Replaces commands/ |
agents | path or array of .md file paths (no directories) | Replaces agents/ |
hooks | path, inline object, or array mixing both | Merges with hooks/hooks.json |
mcpServers | path, .mcpb/.dxt bundle path or URL, inline map, or array | Merges with .mcp.json; later names replace earlier |
lspServers | path, inline map, or array | Merges with .lsp.json; later names replace earlier |
outputStyles | path or array (files or folders) | Replaces output-styles/ |
workflows | path or array (.js files or folders) | Replaces workflows/. See Workflows |
experimental.themes | path or array | Replaces themes/. A top-level themes key still works but triggers a validator warning |
experimental.monitors | path to a JSON file, or the inline array | Replaces monitors/monitors.json. Top-level monitors still works with a warning. Monitors only run interactively and not on Bedrock, Google Cloud's Agent Platform or Foundry |
experimental.evals | path or array | Eval case folder if not evals/. Overridden by claude plugin eval --eval-dir. See Plugin evals |
The experimental container exists because those shapes may still change.
Naming rules
name must be non-empty and contain no spaces, @, :, path separators, control characters or bidirectional formatting characters. Components are namespaced with it: an agent changelog-writer in site-release becomes site-release:changelog-writer.
The validator also stops names that impersonate Anthropic. It ignores case and collapses runs of separators:
| Pattern | Result |
|---|---|
Begins claude-, anthropic-, anthropics- or cc-plugin- | Error |
Exactly claude, anthropic, anthropics, claude-code or claude-mods | Error |
official next to claude or anthropic (e.g. claude-official-kit) | Error |
claude, anthropic or anthropics as a whole word elsewhere (e.g. tools-for-claude) | Warning |
Error text: Plugin name "<name>" is reserved: it passes as one of Anthropic's own. Warning text: Plugin name "<name>" reads as one of Anthropic's own. claude plugin init and claude plugin tag refuse error-level names. Those are the only commands that check; such a plugin still installs and loads.
Component shapes in detail
Path-only components
agents, skills, outputStyles, workflows and experimental.themes take a single path or an array. agents items must be .md files; skills items must be folders; the rest accept files or folders.
{
"agents": ["./agents/changelog-writer.md", "./agents/qa-reviewer.md"],
"skills": ["./", "./client-specific/"],
"workflows": "./flows/"
}
commands
Paths point at flat .md files or folders of them. The object form maps a command name to its definition, and the key becomes the command after the plugin prefix ("preflight" in site-release runs as /site-release:preflight).
Each object entry must set exactly one of source or content:
| Field | Type | Purpose |
|---|---|---|
source | string | Path to the command's Markdown file |
content | string | Inline Markdown body instead of a file |
description | string | Shown alongside the command |
argumentHint | string | Hint after the name, such as [branch] |
model | string | Default model when the command runs |
allowedTools | string array | Tools usable without prompting |
For new plugins I would put most behaviour in skills; commands remain useful for short, fixed prompts.
hooks
Accepts a path to a JSON file, an inline object, or an array mixing both. The inline form is the bare event map, shaped like hooks in settings.json (see Hooks reference). A file must wrap the event map in a top-level "hooks" key, exactly like hooks/hooks.json; a file without the wrapper fails to load. Everything merges with hooks/hooks.json if present.
{
"hooks": [
"./config/release-hooks.json",
{
"Stop": [
{
"hooks": [
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/remind-tag.sh" }
]
}
]
}
]
}
And the referenced file:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/block-force-push.sh" }
]
}
]
}
}
mcpServers
.mcp.json at the root loads first, then each declared value in order, with later server names replacing earlier ones. Server config fields are described in MCP.
| Form | Example | Behaviour |
|---|---|---|
| JSON file | "./mcp/servers.json" | Read as an mcpServers map |
| Bundle file | "./vendor/search.mcpb" | Extracted to .mcpb-cache/ under the plugin root, then read |
| Bundle URL | "https://downloads.example.com/search.mcpb" | Downloaded into .mcpb-cache/, then read |
| Inline map | { "cms": { "command": "node", "args": ["..."] } } | Used directly |
Bundle paths and URLs must end in .mcpb or .dxt.
lspServers
.lsp.json loads first, then each declared config in order; later names replace earlier. Each config is strict:
| Field | Required | Notes |
|---|---|---|
command | yes | Server binary. No spaces unless the value begins with /; put arguments in args |
extensionToLanguage | yes | At least one mapping of extension (starting with .) to LSP language id |
args | no | Arguments |
transport | no | stdio (default) or socket. socket is accepted but every server actually runs over stdio |
env | no | Environment for the process |
initializationOptions | no | Sent with initialize |
settings | no | Sent via workspace/didChangeConfiguration |
workspaceFolder | no | Workspace folder path |
startupTimeout | no | Milliseconds to wait for start-up (positive integer) |
shutdownTimeout | no | Milliseconds before a graceful shutdown is forced. No timeout if unset |
requestTimeout | no | Milliseconds to wait for a reply. Default 60000. Needs v2.1.288 or later |
restartOnCrash | no | Default true. false leaves a crashed server stopped |
maxRestarts | no | Restart attempts before giving up (zero or more) |
diagnostics | no | Push diagnostics into context after edits. Default true |
{
"lspServers": {
"python": {
"command": "pyright-langserver",
"args": ["--stdio"],
"extensionToLanguage": { ".py": "python", ".pyi": "python" },
"requestTimeout": 30000
}
}
}
Code intelligence plugins covers Anthropic's published language-server plugins.
monitors
experimental.monitors takes a JSON file path or the inline array; if omitted, monitors/monitors.json loads when present. Each entry is strict:
| Field | Required | Notes |
|---|---|---|
name | yes | Unique within the plugin |
command | yes | Shell command run as a persistent background process in the session's working directory |
description | yes | Shown in the task panel and in notification summaries |
when | no | "always" (default) starts at session start and on plugin reload; "on-skill-invoke:<skill>" starts the first time that skill runs |
{
"experimental": {
"monitors": [
{
"name": "lighthouse-watch",
"command": "\"${CLAUDE_PLUGIN_ROOT}\"/scripts/watch-lighthouse.sh",
"description": "Lighthouse score regressions on the preview URL",
"when": "on-skill-invoke:release"
}
]
}
}
Monitor commands cannot reference ${user_config.*} (see below).
Path rules
Component paths are relative to the plugin root and must begin with ./; commands/foo.md without the prefix fails. Two exceptions: skills also accepts "." (equivalent to "./", though versions before v2.1.221 reject ".", so use "./" for older clients), and mcpServers accepts https:// bundle URLs.
experimental.evals is not a component path. It names a folder below the root, with or without ./, only the first array item is used, and claude plugin eval checks it at run time.
Containment and existence
Every component path must resolve inside the plugin root and must exist:
| Problem | /plugin Errors tab | Validator |
|---|---|---|
Escapes the root (usually via ..) | <component> path escapes plugin directory: <path> | Path contains ".." which could be a path traversal attempt |
| Does not exist | <component> path not found: <path> | Path not found |
The validator checks outputStyles, lspServers, monitors and themes paths from v2.1.283.
Replace, add or merge
- Replace:
commands,agents,outputStyles,workflows,experimental.themes,experimental.monitors. Setting one means the default folder is not scanned. To keep it, list it:"commands": ["./commands/", "./more-commands/"]. - Add:
skills.skills/is still scanned. - Merge:
hooks,mcpServers,lspServers. The default file loads first.
If a default folder exists and the replacing key is set, you get the warning Default <folder>/ folder is ignored because the manifest sets "<key>" in claude plugin list and /plugin. Pointing the key at something inside that folder (for example "commands": ["./commands/preflight.md"]) avoids the warning.
User configuration
userConfig lets a plugin collect values from the user at enable time, so nobody has to hand-edit settings. Keys must be identifiers: letters, digits and underscores, not starting with a digit. Each option is strict:
| Field | Required | Notes |
|---|---|---|
type | yes | string, number, boolean, directory or file |
title | yes | Label in the dialog |
description | yes | Help text under the field |
required | no | Refuses an empty value when true |
default | no | String, number, boolean or string array |
options | no | For string: the allowed values, shown as a picker in /config. Needs v2.1.271 or later |
multiple | no | For string: allow an array |
sensitive | no | Masks input and stores the value in the OS credential store instead of settings |
min, max | no | Bounds for number |
Every option of every enabled plugin also gets a row in /config (v2.1.269 or later), except sensitive options and multiple lists.
Fixed choices
{
"userConfig": {
"region": {
"type": "string",
"title": "Hosting region",
"description": "Where preview deployments are created",
"options": ["lon1", "fra1", "ams3"],
"default": "lon1"
}
}
}
options only applies to a plain string field (not multiple, not sensitive). Each option is a plain label of 1 to 64 characters. Set default to one of them, or required: true. Invalid options stop the plugin loading, and declaring options anywhere means users on versions before v2.1.271 cannot load the plugin at all.
Where values are kept
Non-sensitive values go under pluginConfigs in the user's settings.json; sensitive ones go to the platform credential store. The settings reference lists which settings files pluginConfigs is read from.
Using a value
${user_config.KEY}is substituted in MCP server config, LSP server config, exec-form hookargs, and skill and agent content. In skill and agent content only non-sensitive values are substituted; sensitive ones become a placeholder.CLAUDE_PLUGIN_OPTION_<KEY>(key uppercased) is exported to hook processes for every option. Forcms_token, a shell-form hook reads$CLAUDE_PLUGIN_OPTION_CMS_TOKEN.
Fields that pass through a shell
Shell-form hook commands, monitor commands and MCP headersHelper refuse ${user_config.*}, because the shell would re-parse whatever was substituted. A component that tries fails with an error (see Errors) rather than running. Alternatives:
| Field | How to get the value there |
|---|---|
| Shell-form hook command | Switch to exec form with args, or read CLAUDE_PLUGIN_OPTION_<KEY> |
| Monitor command | Not via Claude Code: monitors do not receive CLAUDE_PLUGIN_OPTION_<KEY>, so the script must fetch it itself |
MCP headersHelper | Not via Claude Code: the helper gets CLAUDE_PLUGIN_ROOT, CLAUDE_CODE_MCP_SERVER_NAME and CLAUDE_CODE_MCP_SERVER_URL, but no option values |
Channels
channels declares message channels (a bridge to a chat app, for instance) and lets Claude Code prompt for their configuration when the plugin is enabled. Each entry is strict and bound to one of the plugin's MCP servers:
| Field | Required | Notes |
|---|---|---|
server | yes | Key of an MCP server in this plugin's mcpServers |
displayName | no | Title in the configuration dialog; defaults to the server name |
userConfig | no | Same shape as top-level userConfig. Values substitute into ${user_config.KEY} in that server's env |
{
"mcpServers": {
"teams-bridge": {
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/channel/teams.js"],
"env": { "TEAMS_WEBHOOK_SECRET": "${user_config.webhook_secret}" }
}
},
"channels": [
{
"server": "teams-bridge",
"displayName": "Microsoft Teams",
"userConfig": {
"webhook_secret": {
"type": "string",
"title": "Webhook secret",
"description": "Shared secret for the outgoing webhook",
"sensitive": true
}
}
}
]
}
How a channel server pushes messages into a session is covered in the channels reference.
Path variables
| Variable | Value | Use for |
|---|---|---|
${CLAUDE_PLUGIN_ROOT} | Absolute path of the installed version | Bundled scripts, binaries and config |
${CLAUDE_PLUGIN_DATA} | ~/.claude/plugins/data/<id>/, created on first reference and kept across updates. <id> is the plugin id with every character other than letters, digits, _ and - turned into - | Installed dependencies, generated files, caches |
${CLAUDE_PROJECT_DIR} | The project root | Project-local scripts and config |
${CLAUDE_PLUGIN_ROOT} moves on every update, so never store state there. ${CLAUDE_PLUGIN_DATA} is deleted by default when the plugin is uninstalled from its last scope; --keep-data and other exceptions are on the plugin CLI reference.
Where they resolve
| Component | ${...} substituted in | Also exported to the process |
|---|---|---|
| Hook commands | command and args | CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_OPTION_<KEY> |
| Monitor commands | command | Nothing |
| MCP stdio servers | command, args, env | CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA |
MCP http, sse, ws servers | url, headers, headersHelper | n/a |
| LSP servers | command, args, env, workspaceFolder | CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_PROJECT_DIR |
| Skill, command and agent Markdown | Anywhere in the body | n/a |
Commands Claude runs with the Bash tool (in the main session or a subagent) do not have these variables. Reference them in skill or agent Markdown instead and they are substituted when the content loads.
Quoting
Keep each substituted path as one argument. In hooks, prefer exec form with args, which needs no quoting. In shell-form hooks and monitor commands, wrap the variable in double quotes, as in the examples above. The validator warns about an unquoted variable in a shell-form hook unless that hook sets shell to "powershell". On Windows the substituted paths use forward slashes so shells do not treat backslashes as escapes.
Standard layout
| Component | Default location | Notes |
|---|---|---|
| Manifest | .claude-plugin/plugin.json | Optional |
| Skills | skills/ | One <name>/SKILL.md each. A plugin with SKILL.md at its root, no skills/ folder and no skills key loads as a single skill |
| Commands | commands/ | Flat Markdown files; prefer skills for new work |
| Agents | agents/ | Markdown files; subfolders become part of the agent name |
| Hooks | hooks/hooks.json | |
| MCP servers | .mcp.json | |
| LSP servers | .lsp.json | |
| Output styles | output-styles/ | |
| Workflows | workflows/ | .js files |
| Themes | themes/ | JSON files |
| Monitors | monitors/monitors.json | |
| Executables | bin/ | On the Bash tool's PATH while enabled, so Claude can call them by name. claude.ai and Cowork refuse plugins with a top-level bin/, including via organisation sync |
| Settings | settings.json | agent and subagentStatusLine defaults |
A plugin using every default, plus a scripts/ folder its hooks call:
site-release/
├── .claude-plugin/plugin.json
├── skills/release/SKILL.md
├── commands/preflight.md
├── agents/changelog-writer.md
├── hooks/hooks.json
├── monitors/monitors.json
├── output-styles/release-notes.md
├── themes/brand-dark.json
├── workflows/release-audit.js
├── bin/sr
├── scripts/block-force-push.sh
├── settings.json
├── .mcp.json
└── .lsp.json
A CLAUDE.md at the plugin root is not loaded as context and the validator warns about it. Put instructions you want in context inside a skill.
Marketplace entries versus the manifest
A marketplace entry accepts its own fields (including strict) plus every field on this page except the directory listing fields. strict (default true) controls whether the entry may add components to a plugin that has its own plugin.json:
| Situation | Outcome |
|---|---|
No plugin.json | The entry is the manifest, whatever strict says. Entry hooks only works as an inline object; a path or array gives a not yet supported in a marketplace entry error |
plugin.json exists, strict true or unset | The manifest loads and the entry's commands, agents, skills, outputStyles and themes are appended. For hooks, the entry's matchers for an event replace the manifest's for that event; events only in the manifest are kept |
plugin.json exists, strict: false | If the entry declares any of commands, agents, skills, hooks, outputStyles or themes, the plugin fails with Plugin <name> has conflicting manifests |
When an entry whose source is the marketplace root lists particular skills subfolders, only those load and skills/ is not scanned. (In a manifest, skills adds to the default instead.)
Some fields have fixed precedence regardless of strict:
- The entry's
defaultEnabledand display fields such asdisplayNamebeat the manifest's. - The manifest's
versionbeats the entry's. - If the names differ,
enabledPluginsuses the entry name and components are namespaced by the manifest name.