Skip to content

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 (userConfig options, channels entries, lspServers configs and monitors entries) 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
ResultMeaning
Validation passedLoads cleanly
Validation passed with warningsLoads, 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 failedA 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

FieldTypeNotes
$schemastringJSON Schema URL for editor completion. Ignored at load
namestringRequired. Kebab-case identifier; every component is namespaced under it. See naming rules
displayNamestringFriendly label shown in the UI instead of name. Can contain spaces and capitals. Not used for lookup. A marketplace entry's displayName overrides it
versionstringFree-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
descriptionstringOne-line summary
authorobjectname (required), optional email and url
homepagestringDocumentation URL. Must parse as a URL or the plugin fails to load
repositorystringSource URL. Not validated
licensestringSPDX identifier, e.g. MIT
keywordsstring arrayDiscovery tags
metadataobjectAnything 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

FieldTypeNotes
defaultEnabledbooleanWhether 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
dependenciesarrayPlugins 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
settingsobjectSettings applied while enabled. Only agent and subagentStatusLine take effect; others are dropped. A root-level settings.json overrides this key
userConfigobjectValues to prompt for. See User configuration
channelsarrayMessage channels bound to the plugin's MCP servers. See Channels
typespathA .d.ts file declaring the $.state values and $ nouns of a mod. See Mods reference

Component locations

FieldAcceptsRelationship to default
skillspath 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
commandspath, array of paths, or object mapReplaces commands/
agentspath or array of .md file paths (no directories)Replaces agents/
hookspath, inline object, or array mixing bothMerges with hooks/hooks.json
mcpServerspath, .mcpb/.dxt bundle path or URL, inline map, or arrayMerges with .mcp.json; later names replace earlier
lspServerspath, inline map, or arrayMerges with .lsp.json; later names replace earlier
outputStylespath or array (files or folders)Replaces output-styles/
workflowspath or array (.js files or folders)Replaces workflows/. See Workflows
experimental.themespath or arrayReplaces themes/. A top-level themes key still works but triggers a validator warning
experimental.monitorspath to a JSON file, or the inline arrayReplaces 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.evalspath or arrayEval 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:

PatternResult
Begins claude-, anthropic-, anthropics- or cc-plugin-Error
Exactly claude, anthropic, anthropics, claude-code or claude-modsError
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:

FieldTypePurpose
sourcestringPath to the command's Markdown file
contentstringInline Markdown body instead of a file
descriptionstringShown alongside the command
argumentHintstringHint after the name, such as [branch]
modelstringDefault model when the command runs
allowedToolsstring arrayTools 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.

FormExampleBehaviour
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:

FieldRequiredNotes
commandyesServer binary. No spaces unless the value begins with /; put arguments in args
extensionToLanguageyesAt least one mapping of extension (starting with .) to LSP language id
argsnoArguments
transportnostdio (default) or socket. socket is accepted but every server actually runs over stdio
envnoEnvironment for the process
initializationOptionsnoSent with initialize
settingsnoSent via workspace/didChangeConfiguration
workspaceFoldernoWorkspace folder path
startupTimeoutnoMilliseconds to wait for start-up (positive integer)
shutdownTimeoutnoMilliseconds before a graceful shutdown is forced. No timeout if unset
requestTimeoutnoMilliseconds to wait for a reply. Default 60000. Needs v2.1.288 or later
restartOnCrashnoDefault true. false leaves a crashed server stopped
maxRestartsnoRestart attempts before giving up (zero or more)
diagnosticsnoPush 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:

FieldRequiredNotes
nameyesUnique within the plugin
commandyesShell command run as a persistent background process in the session's working directory
descriptionyesShown in the task panel and in notification summaries
whenno"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 tabValidator
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:

FieldRequiredNotes
typeyesstring, number, boolean, directory or file
titleyesLabel in the dialog
descriptionyesHelp text under the field
requirednoRefuses an empty value when true
defaultnoString, number, boolean or string array
optionsnoFor string: the allowed values, shown as a picker in /config. Needs v2.1.271 or later
multiplenoFor string: allow an array
sensitivenoMasks input and stores the value in the OS credential store instead of settings
min, maxnoBounds 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 hook args, 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. For cms_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:

FieldHow to get the value there
Shell-form hook commandSwitch to exec form with args, or read CLAUDE_PLUGIN_OPTION_<KEY>
Monitor commandNot via Claude Code: monitors do not receive CLAUDE_PLUGIN_OPTION_<KEY>, so the script must fetch it itself
MCP headersHelperNot 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:

FieldRequiredNotes
serveryesKey of an MCP server in this plugin's mcpServers
displayNamenoTitle in the configuration dialog; defaults to the server name
userConfignoSame 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

VariableValueUse for
${CLAUDE_PLUGIN_ROOT}Absolute path of the installed versionBundled 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 rootProject-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 inAlso exported to the process
Hook commandscommand and argsCLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_OPTION_<KEY>
Monitor commandscommandNothing
MCP stdio serverscommand, args, envCLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA
MCP http, sse, ws serversurl, headers, headersHelpern/a
LSP serverscommand, args, env, workspaceFolderCLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, CLAUDE_PROJECT_DIR
Skill, command and agent MarkdownAnywhere in the bodyn/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

ComponentDefault locationNotes
Manifest.claude-plugin/plugin.jsonOptional
Skillsskills/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
Commandscommands/Flat Markdown files; prefer skills for new work
Agentsagents/Markdown files; subfolders become part of the agent name
Hookshooks/hooks.json
MCP servers.mcp.json
LSP servers.lsp.json
Output stylesoutput-styles/
Workflowsworkflows/.js files
Themesthemes/JSON files
Monitorsmonitors/monitors.json
Executablesbin/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
Settingssettings.jsonagent 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:

SituationOutcome
No plugin.jsonThe 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 unsetThe 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: falseIf 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 defaultEnabled and display fields such as displayName beat the manifest's.
  • The manifest's version beats the entry's.
  • If the names differ, enabledPlugins uses the entry name and components are namespaced by the manifest name.