Skip to content

Plugin components

Every component a Claude Code plugin can carry, where its files go, how it is named once loaded, and the manifest key that overrides the default.

Once you have a plugin that loads (see /docs/plugins/create), this page is the catalogue of what you can put in it. For each component you get the default location, a fresh example, the name users will see, and the manifest key that changes the default. Nothing here is mandatory: add only what your plugin needs.

After adding anything, run /reload-plugins in an open session or start a new one. To check files before loading, run claude plugin validate . from the plugin folder.

The full map

Here is a hypothetical ops-kit plugin with one of everything in its default place:

ops-kit/
├── .claude-plugin/plugin.json     manifest
├── skills/triage/SKILL.md         skill        -> /ops-kit:triage
├── commands/status.md             command      -> /ops-kit:status
├── agents/incident-scribe.md      subagent     -> ops-kit:incident-scribe
├── hooks/hooks.json               hooks (and optional JS module = mod)
├── monitors/monitors.json         background watchers
├── output-styles/oncall.md        output style -> ops-kit:oncall
├── themes/pager.json              colour theme
├── workflows/sweep-alerts.js      workflow     -> /ops-kit:sweep-alerts
├── bin/ops-env                    executable on the Bash PATH
├── scripts/                       helpers your hooks call (convention only)
├── settings.json                  default settings (agent, subagentStatusLine)
├── .mcp.json                      MCP servers  -> plugin:ops-kit:<server>
└── .lsp.json                      language servers
ComponentDefault locationUser-facing nameManifest key
Skillsskills/<dir>/SKILL.md/<plugin>:<dir>skills (adds to the scan)
Commandscommands/<file>.md/<plugin>:<file>commands (replaces the scan)
Agentsagents/**/*.md<plugin>:<name>agents (replaces the scan)
Hookshooks/hooks.jsonn/ahooks (both load)
MCP servers.mcp.jsonplugin:<plugin>:<server>mcpServers
LSP servers.lsp.jsonn/alspServers
Executablesbin/bare command namen/a
Default settingssettings.jsonn/asettings
Themesthemes/<slug>.jsontheme's name in /themeexperimental.themes
Output stylesoutput-styles/<name>.md<plugin>:<name> in /output-styleoutputStyles
Channelsmanifest onlyn/achannels
Monitorsmonitors/monitors.jsonn/aexperimental.monitors
Workflowsworkflows/*.js/<plugin>:<name>n/a

Field-level detail for every key is in /docs/plugins/manifest-reference.

Manifest

.claude-plugin/plugin.json holds metadata and any userConfig options. A plugin loads without one, but in practice always add it. Only name is required:

{
  "name": "ops-kit",
  "version": "2.3.0",
  "description": "On-call helpers: triage, incident notes, alert sweeps"
}

Skills

A skill is a SKILL.md in its own folder under skills/. Claude reads every skill's description and loads the body when the task matches; users can also run it by name.

---
description: Triage a production alert. Use when the user pastes an alert, a stack trace from prod, or says something is down.
---

1. Identify the service and the first failing timestamp.
2. Check recent deploys with `git log --since="6 hours ago" --oneline`.
3. Propose the single most likely cause and one command to confirm it.

Naming and invocation rules:

  • Command name is /<plugin>:<directory>. Setting name in frontmatter replaces the last part but the plugin prefix stays.
  • Who can invoke (Claude, the user, or both) is controlled in frontmatter; see /docs/skills.
  • Extra skill folders can be listed in the skills manifest key. Unlike commands and agents, these add to the default skills/ scan.
  • Single-skill plugin: with no skills/ folder and no skills key, a SKILL.md at the plugin root loads as one skill. Give it a name, otherwise a marketplace install names it after its cache directory.

Two things that catch people out:

  • A CLAUDE.md at the plugin root is not loaded. claude plugin validate warns CLAUDE.md at the plugin root is not loaded as project context. Put instructions in a skill.
  • If a rule must hold every single time (never edit migrations/, say), use a hook, not a skill. Skills are guidance; hooks are enforcement. /docs/features-overview compares them.

Commands

Commands are single Markdown files, the older format that skills replace. A skill does everything a command does and can carry supporting files, so use commands/ only for files migrated from .claude/commands/.

commands/<file>.md becomes /<plugin>:<file>. Subfolders add a segment: commands/db/reset.md is /ops-kit:db:reset. Frontmatter is the same as for skills.

The commands manifest key replaces the folder scan. It accepts a path, an array of paths, or an object mapping command names to a source file or inline content. Inline is handy for tiny commands:

{
  "name": "ops-kit",
  "commands": {
    "status": {
      "description": "Summarise open incidents",
      "content": "List open GitHub issues labelled incident, newest first, one line each."
    }
  }
}

Agents

Each Markdown file under agents/ defines a subagent: frontmatter names it and says when to use it, the body is its system prompt.

---
name: incident-scribe
description: Writes a timeline and post-incident summary from the conversation so far. Use when an incident is resolved.
model: haiku
tools: Read, Grep
---

You write blameless incident notes. Produce: Summary, Timeline (UTC), Impact, Root cause, Follow-ups.

This loads as ops-kit:incident-scribe, and users can call it explicitly with @agent-ops-kit:incident-scribe. The name is <plugin>:<name>, using frontmatter name or the file name if absent. The agents manifest key replaces the folder scan.

Subfolders

Agents load recursively, with subfolders joined by colons. agents/review/perf.md in ops-kit becomes ops-kit:review:perf. Frontmatter name: latency changes only the last segment (ops-kit:review:latency). A file listed directly in the agents manifest key drops the subfolders: "agents": "./extra/review/perf.md" loads as ops-kit:perf.

Frontmatter support

StatusFields
Supportedname, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, omitClaudeMd, isolation (only "worktree"), color, and cacheTtl under experimental
IgnoredpermissionMode, hooks, mcpServers, initialPrompt

An agent file cannot bring its own hooks or MCP servers; add those at plugin level. If an agent's frontmatter fails to parse, it still loads with all fields ignored, named after the file and described as Agent from <plugin> plugin. claude plugin validate finds these.

Hooks

Hooks run automatically at lifecycle points: a shell command, an HTTP request, an MCP tool call, a model prompt or a subagent. Put them in hooks/hooks.json under a top-level "hooks" key with exactly the shape of hooks in settings.json, so you can paste existing hooks straight in.

This one blocks Claude from touching anything in infra/prod/:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/guard-prod.sh\"" }
        ]
      }
    ]
  }
}
#!/bin/bash
# scripts/guard-prod.sh  (chmod +x)
path=$(jq -r '.tool_input.file_path')
case "$path" in
  */infra/prod/*) echo "Edits to infra/prod are blocked by ops-kit" >&2; exit 2 ;;
esac

Hooks in hooks/hooks.json and in the hooks manifest key both load. A successful PostToolUse hook (exit 0) is silent in the transcript, so confirm it ran with debug logging or by its side effects. Every event and payload is in /docs/hooks.

To write hooks as JavaScript functions that run in-process and can draw UI, list a module file under a modules key in the same hooks/hooks.json. That makes the plugin a mod; see /docs/plugins/mods/create.

Behaviour worth knowing

  • Always on. Plugin hooks register when the session loads the plugin and fire on their events from then on, whether or not any of the plugin's skills are used. Narrow the matcher to limit them.
  • Environment. Every hook process gets CLAUDE_PLUGIN_ROOT, CLAUDE_PLUGIN_DATA, and CLAUDE_PLUGIN_OPTION_<KEY> for each user config value.
  • Quoting. Without args, command goes through a shell, so wrap ${CLAUDE_PLUGIN_ROOT} paths in double quotes. With args, each element is a single argument and needs no quoting.
  • Matching your own MCP tools. Tools from the plugin's servers are named mcp__plugin_<plugin>_<server>__<tool>. Use that full form; a matcher on just the server name never fires.

If a hook never fires, see /docs/plugins/troubleshooting.

MCP servers

Declare servers in .mcp.json at the plugin root, the same shape as a project .mcp.json. The mcpServers wrapper is optional.

{
  "mcpServers": {
    "pager": {
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/servers/pager.js"],
      "env": { "PAGER_TOKEN": "${user_config.pager_token}" }
    }
  }
}
ThingFormatExample
Server name (in /mcp, mcp_tool hooks)plugin:<plugin>:<server>plugin:ops-kit:pager
Tool name (permissions, matchers)mcp__plugin_<plugin>_<server>__<tool>mcp__plugin_ops-kit_pager__ack
  • ${CLAUDE_PLUGIN_ROOT} and the other path variables substitute in command, args and env.
  • On /reload-plugins, unchanged servers keep their connection, changed ones reconnect and removed ones disconnect.
  • claude plugin validate (v2.1.281+) reports entries that would be dropped at load time as errors.
  • The mcpServers manifest key accepts an inline map, a path to a JSON file, or an array of those. Manifest servers replace same-named .mcp.json servers.

A local stdio server works in Claude Code and in local Cowork sessions in the desktop app, but not on claude.ai. To reach claude.ai users, reference a remote https:// server, which they are offered as a connector.

Packaged MCPB servers

mcpServers can also point at a packaged MCPB bundle (.mcpb, or the older .dxt), as a path inside the plugin or an https:// URL:

{ "name": "ops-kit", "mcpServers": "./servers/statuspage.mcpb" }

The server is named from the bundle's own manifest. If that manifest declares a required user_config value with nothing saved, the server does not start and Errors shows Bundled MCP server "<name>" was not started: it needs configuration. Users fix that via Installed > Configure in /plugin, or at install time with claude plugin install ... --config <server>.<key>=<value> (v2.1.285+, bundles packaged inside the plugin only). More on transports and auth in /docs/mcp.

LSP servers

LSP servers feed Claude diagnostics and symbol navigation. If an official code intelligence plugin covers your language, use that. Otherwise, add .lsp.json at the root. It maps server names straight to config with no wrapper:

{
  "terraform-ls": {
    "command": "terraform-ls",
    "args": ["serve"],
    "extensionToLanguage": {
      ".tf": "terraform",
      ".tfvars": "terraform-vars"
    }
  }
}

command is the binary name, args its arguments, and extensionToLanguage needs at least one extension starting with ..

  • claude plugin validate does not read .lsp.json. One bad entry makes the whole file skip at load, with Invalid LSP server config for ".lsp.json" on Errors.
  • Your plugin does not install the binary. If it is not on the user's PATH, claude --debug logs LSP server <name> failed to start.
  • One server per extension. When two enabled servers claim the same extension, the first registered wins and Errors shows LSP server "<name>" is not used for <ext> files.
  • lspServers in the manifest accepts an inline map, a path, or an array, and adds to .lsp.json; same-named manifest entries replace file entries. transport, timeouts and restart fields are in /docs/plugins/manifest-reference.

Log to stderr, never stdout. Claude Code treats stdout as protocol only and accepts headers up to 64 KiB and bodies up to 32 MiB. Exceeding either, or printing non-protocol text to stdout, disconnects the server and counts as a crash for restartOnCrash and maxRestarts.

Executables

Anything in bin/ is on the Bash tool's PATH while the plugin is enabled, so Claude can run it by bare name:

#!/bin/bash
# bin/ops-env: print which environment the current kube context targets
kubectl config current-context | sed 's/.*-//'

chmod +x bin/ops-env, load the plugin, and ask Claude to run ops-env. Plugin bin/ folders come after the user's own PATH entries, so a plugin cannot shadow git or ls.

claude.ai and Cowork refuse to install a plugin with a top-level bin/ folder, including through claude.ai organisation settings. Keep that in mind if you target both.

Default settings

A settings.json at the plugin root (or the same object inline under the manifest's settings key) applies while the plugin is enabled. Only two keys take effect: agent and subagentStatusLine. Everything else is dropped.

{ "agent": "incident-scribe" }

With that, the session's main thread runs as the plugin's incident-scribe agent, using its prompt, tool restrictions and model. See /docs/settings-reference and, for subagentStatusLine, /docs/statusline.

Precedence:

  • If settings.json sets at least one supported key, it wins and the manifest settings is ignored.
  • Plugin defaults are the lowest settings layer: a user's own agent in ~/.claude/settings.json overrides yours.
  • If two plugins set the same key, the last loaded wins and claude --debug logs overrides setting.

Themes and output styles

Both appear alongside the user's own in the usual pickers. Setting the manifest key replaces the folder scan.

ThemeOutput style
Filethemes/<slug>.jsonoutput-styles/<name>.md
FormatSame as custom themes in ~/.claude/themes/Same as custom output styles, with name and description frontmatter
Shows in/theme, under its name/output-style as <plugin>:<name>
Manifest keyexperimental.themesoutputStyles
{
  "name": "Pager Duty Red",
  "base": "dark",
  "overrides": { "claude": "#ff6b6b", "error": "#ffd93d" }
}

Plugin themes are read-only. If a user edits one in /theme, the edit is saved as a copy in their own themes folder. See /docs/terminal-config and /docs/output-styles.

Channels

A channel lets an outside system, such as a chat app, push messages into a session. In a plugin, a channel is an MCP server plus a channels entry that binds to it and can request its own config:

{
  "name": "ops-kit",
  "mcpServers": {
    "slackbridge": {
      "command": "node",
      "args": ["${CLAUDE_PLUGIN_ROOT}/channels/slack.js"],
      "env": { "SLACK_BOT_TOKEN": "${user_config.slack_bot_token}" }
    }
  },
  "channels": [
    {
      "server": "slackbridge",
      "userConfig": {
        "slack_bot_token": {
          "type": "string",
          "title": "Slack bot token",
          "description": "xoxb- token for the incident bot",
          "sensitive": true
        }
      }
    }
  ]
}

server must match an mcpServers key. Per-channel userConfig uses the same shape as the top-level one. What the server must implement is in /docs/channels-reference.

Monitors

A monitor is a background shell command that runs for the session; whatever it prints reaches Claude as a notification. Good for "tell me if the error log moves".

[
  {
    "name": "deploy-watch",
    "command": "tail -F ./var/log/deploy.log",
    "description": "Deploy pipeline output"
  }
]

Saved as monitors/monitors.json. It runs in a shell in the session's working directory, with your full permissions and outside the sandbox. A when field can start it the first time a named skill runs instead of at session start.

Limits:

  • Interactive only. Never started under -p, nor where provider or telemetry settings make the Monitor tool unavailable.
  • No user config. command gets path variables and ${ENV_VAR} but never ${user_config.*}; referencing one stops the monitor starting. Monitor processes do not receive CLAUDE_PLUGIN_OPTION_<KEY>.
  • Keeps running. Disabling the plugin mid-session does not stop a running monitor; it ends with the session.

experimental.monitors in the manifest takes the array inline or as a path and is read instead of the file.

Workflows

workflows/*.js files are workflow scripts: a meta export then a body that orchestrates subagents. A file whose meta.name is sweep-alerts runs as /ops-kit:sweep-alerts.

Ask users for configuration

Rather than asking users to hand-edit settings, declare what you need under userConfig. Each option shows in a dialog with title as the label and description underneath. Mark secrets with "sensitive": true so input is masked and stored in secure storage instead of settings.json.

{
  "name": "ops-kit",
  "userConfig": {
    "pager_base_url": {
      "type": "string",
      "title": "Pager API URL",
      "description": "Base URL for your paging provider"
    },
    "pager_token": {
      "type": "string",
      "title": "Pager API token",
      "description": "Read and acknowledge scope is enough",
      "sensitive": true
    }
  }
}

The dialog opens for any unset option when a user installs through /plugin, runs /plugin install <plugin>@<marketplace> in a session, or enables the plugin on the Installed tab. /plugin configure <plugin>@<marketplace> reopens it any time. VS Code's Manage plugins dialog shows a form after install, and a gear icon reopens it.

claude plugin install in the shell never prompts. Pass --config KEY=VALUE at install, or pipe JSON into claude plugin configure --values-stdin afterwards. Unset options produce a userConfig options not yet set line. Storage locations and which fields reject ${user_config.*} are in /docs/plugins/manifest-reference.

Paths and persistent data

You never know where your plugin will be installed, so use these variables. They substitute in skill, command and agent text, hook and monitor commands, and MCP and LSP config, and are exported to hook, MCP and LSP processes.

VariablePoints toNotes
${CLAUDE_PLUGIN_ROOT}The installed plugin folderChanges on every version update. Do not write state here
${CLAUDE_PLUGIN_DATA}~/.claude/plugins/data/<id>/Survives updates. Created on first reference. Use for node_modules, venvs, caches
${CLAUDE_PROJECT_DIR}The project rootSame value hooks receive

<id> is the plugin identifier with anything other than letters, digits, _ and - replaced by -, so ops-kit@acme-plugins becomes ops-kit-acme-plugins. On Windows the substituted paths use forward slashes.

Installing dependencies once

Marketplace installs get eligible Node.js dependencies installed automatically when cached (see /docs/plugins/loading). If you need to manage it yourself, a SessionStart hook can install into the data folder only when requirements.txt changes. A Python version:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cmp -s \"${CLAUDE_PLUGIN_ROOT}/requirements.txt\" \"${CLAUDE_PLUGIN_DATA}/requirements.txt\" || (python3 -m venv \"${CLAUDE_PLUGIN_DATA}/venv\" && \"${CLAUDE_PLUGIN_DATA}/venv/bin/pip\" install -q -r \"${CLAUDE_PLUGIN_ROOT}/requirements.txt\" && cp \"${CLAUDE_PLUGIN_ROOT}/requirements.txt\" \"${CLAUDE_PLUGIN_DATA}/\")"
          }
        ]
      }
    ]
  }
}

Your MCP server can then launch with ${CLAUDE_PLUGIN_DATA}/venv/bin/python. For Node the same pattern works with package.json, npm install and NODE_PATH set to ${CLAUDE_PLUGIN_DATA}/node_modules in the server's env.