Plugins are the highest-level extension mechanism in Claude Code. They bundle skills, subagents, hooks, MCP servers, and LSP configurations into a single installable package. A team installs one plugin and immediately gets everything configured — no manual setup for each component. This module covers the plugin structure, manifest format, distribution mechanisms, and how to build your own.
Plugin Architecture
A plugin is a directory with a specific structure. The manifest, .claude-plugin/plugin.json, declares the plugin’s identity — but it is optional: if you omit it, Claude Code auto-discovers components in their default locations (skills/, agents/, hooks/, .mcp.json, .lsp.json, etc.). The plugin name then comes from the marketplace entry, or from the directory name when you load the plugin with --plugin-dir. Provide a manifest with an explicit name field when you want stable identity or metadata — for a plugin copied into the cache, the directory name is a version string that changes on every update. That name is also the identity Claude Code deduplicates on: when two enabled plugins resolve to the same name, only one wins and the other’s hooks are left out rather than run twice. As of v2.1.296, /plugin surfaces a note naming the plugin whose hooks were dropped for this reason, so a silent name collision shows up instead of just producing missing behavior. Everything follows conventions Claude Code recognizes:
my-plugin/
├── .claude-plugin/
│ └── plugin.json # Plugin manifest (optional)
├── skills/ # SKILL.md files
│ └── my-skill/
│ └── SKILL.md
├── agents/ # Subagent definitions
│ └── specialist.md
├── commands/ # Legacy command files (also work)
│ └── my-command.md
├── hooks/
│ └── hooks.json # Plugin-scoped hooks
├── .mcp.json # MCP server configs
├── .lsp.json # LSP server configs
├── settings.json # Default settings
└── bin/
└── helper.sh
A plugin that ships exactly one skill can place SKILL.md directly at the plugin root instead of creating a skills/ directory. Claude Code loads it as a single skill and uses the frontmatter name field for the invocation name. Use the skills/ layout for plugins that may grow to more than one skill.
The manifest identifies the plugin and its metadata:
{
"name": "pr-review",
"description": "Complete PR review workflow with security and test coverage checks",
"version": "1.0.0",
"author": {
"name": "Your Name"
},
"repository": "https://github.com/you/pr-review",
"license": "MIT"
}
Plugin commands and plugin-provided skills are namespaced as plugin-name:command-name to avoid conflicts with project-level configuration. Invoke them with the full namespaced form, such as /pr-review:check-security.
Manifest Features
The manifest supports several powerful fields for configuring plugin behavior. userConfig declares user-configurable options. Fields marked sensitive: true are stored in the system keychain rather than plain-text settings:
{
"name": "my-plugin",
"version": "1.0.0",
"userConfig": {
"apiKey": {
"description": "API key for the integration",
"sensitive": true
},
"region": {
"description": "Deployment region",
"default": "us-east-1"
}
}
}
Users don’t hand-edit settings.json to fill in these options — run /plugin configure <plugin> in a session (alias /plugin config, v2.1.147+) to open the plugin’s userConfig dialog and set the values, or be told the plugin declares none. Non-sensitive values are saved under pluginConfigs in settings.json, while sensitive values go to the platform’s secure credential store. Naming a plugin that isn’t installed in the current project prints Plugin "<plugin>" is not installed in this project instead of acting.
Plugins access persistent state via ${CLAUDE_PLUGIN_DATA} (v2.1.78+) — a shell expansion variable that survives plugin updates, making it suitable for caches, state files, and databases. Use ${CLAUDE_PLUGIN_ROOT} to reference paths relative to the plugin installation directory — essential for hooks and MCP configurations:
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/bin/audit.js"
}
]
}
]
}
}
Plugin monitors require Claude Code v2.1.105 or later. Declare them in a monitors/monitors.json file at the plugin root, or inline under the manifest’s experimental.monitors key. The monitors manifest key wires the plugin into the Monitor tool. Point it at a JSON file (or inline the config) and its background watches auto-arm the moment the plugin is enabled at session start, or when one of the plugin’s skills is invoked. This is how a plugin ships “watch CI, flag failures” or “tail the dev server log” behaviour without the user having to set it up:
{
"name": "ci-watcher",
"version": "1.0.0",
"experimental": {
"monitors": "./monitors.json"
}
}
When the manifest sets a custom monitors path, the default monitors/monitors.json location is no longer scanned — specify the default explicitly if you still want it loaded alongside your custom file.
The Monitor tool has its own version floor. On Amazon Bedrock, Claude Platform on AWS, Google Cloud’s Agent Platform, or Microsoft Foundry, or with DISABLE_TELEMETRY or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC set, Monitor requires Claude Code v2.1.310 or later.
LSP support adds real-time language server protocol integration. Put a .lsp.json in the plugin root to configure language servers that provide instant diagnostics, go-to-definition, and symbol search as Claude edits files. The restartOnCrash and shutdownTimeout fields (v2.1.205+) configure graceful shutdown and restart behavior — before v2.1.205, setting either option caused Claude Code to skip that LSP server at startup entirely.
A plugin’s settings.json supports only the agent and subagentStatusLine keys; all other fields are silently ignored.
{
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact"
}
}
}
Mods — Hooks as JavaScript Functions
A mod is a plugin that changes how Claude Code looks and behaves from the inside. The hooks you met in the Hooks module run as a shell command, HTTP request, or prompt configured in a settings file; a mod’s hooks are JavaScript or TypeScript functions that run inside the Claude Code process instead. A plugin that ships such a hooks module is called a mod. Mods require Claude Code v2.1.287 or later and are on by default — claude --version tells you whether yours is new enough.
Running in-process is what lets a mod do things a settings hook, a skill, or an MCP server can’t:
- Draw an interface. Add a pane beside the transcript or a band above the prompt, with tabs, buttons, and text fields — for example a pane that charts how full your context window is after each request.
- Restyle what Claude Code already draws. Replace or restyle a tool call’s row, the spinner, or the dialog Claude asks questions in. The one thing a mod can never touch is the permission prompt.
- Step into a tool call or a request. Hold a tool call while you ask the user a question, answer it without running the tool, or route a single request to a different model.
- Add a command. A
/commandthat runs your function immediately, with no Claude turn, even while Claude is working. - Spawn and steer subagents. Through the
$.agentAPI a mod can register a subagent type, start a subagent — or an agent-team teammate — from its own code with$.agent.spawn, and list the session’s agents with$.agent.list()(each entry carries astatussuch asrunning,completed, orfailed, so inspect it rather than assuming every result is active). It can also step into Claude’s ownagent.spawnevent to choose a spawned agent’s model or block it, or withhold a subagent type from Claude throughagent.offer. - Share state between hooks. A mod’s hooks share the variables in its file, so one hook can count tool calls while another shows the count beside the spinner.
A mod is a plugin, so it has the same shape you already know — a plugin.json manifest plus a hooks/ directory. The difference is that hooks/hooks.json points at a code file (the hooks module) which registers event handlers:
// hooks/register.js — counts tool calls and shows the running total by the spinner
let calls = 0;
export function register(on) {
on("tool.call", async ($, e, next) => {
calls += 1;
$.ui.invalidate("ui.render"); // ask Claude Code to redraw
return next(e); // let the tool run as usual
});
on("ui.render", { component: "Spinner" }, async ($, e, next) => {
return next({ ...e, props: { ...e.props, suffix: ` · tool calls: ${calls}…` } });
});
}
Each hook can observe an event and let it continue, rewrite it before it continues, or answer it so the usual behavior doesn’t run. Anything a hook does beyond its own code — drawing, adding a command, calling a model, reading a file, making a network request — goes through the mods API. That indirection is why claude plugin validate ./some-mod can list a mod’s hooks: and calls: before you ever load it.
When a mod handles a tool.check event — the event where Claude Code decides whether a tool call may run — the event also tells you whether that call is read-only. e.isReadOnly is true when the tool reports the call as read-only; otherwise the field is absent, not false. It requires Claude Code v2.1.304 or later. It is the tool’s own report, not an approval signal: don’t allow a call on this field alone, since WebFetch and Agent report as read-only while still fetching a URL or launching a subagent. The same value works as a matcher, so a handler only receives the calls you care about:
on("tool.check", { tool: "Bash", isReadOnly: true }, async ($, e, next) => {
// only read-only Bash calls reach this handler
return next(e);
});
You install a mod exactly like any other plugin — /plugin install token-chart@your-org, or claude plugin install token-chart@your-org from the shell followed by /reload-plugins. A mod’s hooks run everywhere a session loads the plugin (the terminal, the Desktop app’s Code tab, the VS Code chat panel, claude -p and the Agent SDK, Remote Control, and cloud sessions), but what it draws appears in the terminal, the Desktop app, and the VS Code extension — the extension draws too, from version 2.1.299 (2.1.304 on a session that doesn’t fetch feature flags). Some of Claude Code’s own features are built-in mods — /diff draws its pane as one, and cc-plugin-you-should-know@builtin (off by default) runs a side agent that posts a note above the prompt when it spots something you or Claude might miss on a longer task. Enable the latter with /plugin enable cc-plugin-you-should-know@builtin.
Because a mod runs with your full permissions and is not sandboxed, install mods only from authors and marketplaces you trust — it can read and write your files, spend your usage, and approve tool calls on your behalf. To turn mods off, disable the plugin from /plugin, start a session with --safe-mode to drop every customization at once, or set "disableAllHooks": true in ~/.claude/settings.json to stop them in every session.
CLAUDE_CODE_PLUGIN_DIR_WATCH controls whether Claude Code reloads a mod when the mod’s files change. Reloading applies to a mod you load from a directory with --plugin-dir, and it’s on by default in interactive sessions. Set it to 1 to turn it on in non-interactive sessions as well, or 0 to turn it off in every session. Requires Claude Code v2.1.287 or later.
Distribution and Development
Test a plugin locally with the --plugin-dir flag before distributing. It loads the plugin for that session only — no installation:
claude --plugin-dir ./my-plugin
# Test multiple plugins simultaneously:
claude --plugin-dir ./my-plugin --plugin-dir ./another-plugin
For plugins hosted as .zip archives, --plugin-url fetches and installs them for the current session without permanent installation. Repeat the flag for multiple plugins:
claude --plugin-url https://example.com/my-plugin.zip
claude --plugin-url https://example.com/a.zip --plugin-url https://example.com/b.zip
Only use --plugin-url with URLs you trust — loading remote archives executes third-party code on your machine.
Plugin evals are the plugin’s own eval cases: the experimental.evals manifest key points at the directory that holds them (evals/ by default), and claude plugin eval runs them — --eval-dir overrides that directory for a single run. Claude Code v2.1.299 added a --disallow-tools flag to claude plugin eval, together with a disallowed_tools case field, so specific built-in tools — Agent, for example — can be kept out of eval runs. The flag takes one or more tool names:
# Keep the built-in Agent tool out of the eval run
claude plugin eval --disallow-tools Agent
Use /reload-plugins to hot-reload plugin files during development without restarting the session. This re-reads all manifests, skills, agents, hooks, and MCP configurations instantly.
The /plugin Discover and Browse screens now show a full inventory of what a plugin will install before you commit to it — commands, agents, skills, hooks, and any MCP or LSP servers it ships. This makes vetting a marketplace plugin a one-screen decision: you can confirm a pr-review plugin isn’t quietly registering MCP servers or hooks you didn’t expect, or check that a deploy plugin actually ships the skill you came for. Browse lists what’s already installed with the same breakdown; Discover does the same for the marketplace catalog. The preview is informational only — the plugin still has to be installed before any of its components run.
Plugin distribution follows a marketplace model. The official Anthropic marketplace is claude-plugins-official. Add additional marketplaces with /plugin marketplace add owner/repo-name. Install plugins with /plugin install plugin-name or claude plugin install plugin-name@marketplace:
# Install from official marketplace
/plugin install pr-review
# Install from a marketplace added from GitHub (use the plain owner/repo shorthand — the github: prefix is not documented)
/plugin marketplace add owner/repo-name
/plugin install plugin-name@marketplace-name
# Add a marketplace and install in one step (v2.1.275+): name the source with --marketplace
/plugin install plugin-name --marketplace owner/repo-name
# Install from local path (for testing)
/plugin install ./path/to/plugin
To install from a marketplace you haven’t added yet, name its source with --marketplace (Claude Code v2.1.275+) instead of adding it first with a separate /plugin marketplace add. The source takes the same forms as /plugin marketplace add — a GitHub owner/repo, a git URL, or a local path (it just can’t contain spaces) — and you give the plugin name bare, without an @marketplace suffix. If that marketplace isn’t registered yet, Claude Code shows the source it resolved and asks you to confirm before adding it; declining cancels the install and adds nothing.
When a plugin source is a GitHub owner/repo shorthand, Claude Code checks whether your SSH key authenticates to github.com, then clones over SSH if it does and over HTTPS if it doesn’t, and falls back to HTTPS when the SSH clone fails. Set CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 to skip that check and always clone over HTTPS — the reliable choice for CI runners and containers. The rest of the install flow stays identical:
# Force HTTPS for plugin clones in CI
CLAUDE_CODE_PLUGIN_PREFER_HTTPS=1 claude plugin install owner/repo
For enterprise environments, managed-mcp.json controls which MCP servers plugins can use. The enabledPlugins, extraKnownMarketplaces, strictKnownMarketplaces, and blockedMarketplaces settings in managed policy control which plugins and marketplaces are allowed organization-wide. strictKnownMarketplaces (an allowlist) and blockedMarketplaces (a blocklist) each accept an owner wildcard (Claude Code v2.1.223+): a GitHub entry of the form { "source": "github", "repo": "owner/*" } covers every marketplace repository under that owner in one line — allow an entire trusted org, or block an entire untrusted one, without listing each repo. The matching rules differ between the allowlist and the blocklist, so check the owner-wildcards reference before relying on one. Plugin subagents have restricted frontmatter — they cannot define hooks, mcpServers, or permissionMode to prevent privilege escalation.
allowManagedPermissionRulesOnly is the managed setting that narrows all of this to the managed policy alone: Claude Code keeps only the allow rules in the managed policy and drops allow rules a host supplies as parent settings, as --allowedtools, or in a settings file. As of v2.1.305, plugins that Claude Desktop mounts by path no longer keep their allowed-tools when your organization’s managed settings set this key.
Useful lifecycle commands include claude plugin list, claude plugin enable, claude plugin disable, claude plugin uninstall, claude plugin validate, claude plugin prune (new in v2.1.121, aliased autoremove) which removes auto-installed plugin dependencies that no other installed plugin still requires — plugins you installed directly are never touched; to uninstall a plugin and clean up its dependencies in one step, run claude plugin uninstall <plugin> --prune. claude plugin details <name> shows a plugin’s component inventory grouped as Skills, Agents, Hooks, MCP servers, and LSP servers, along with an estimate of how many tokens it adds to each session. And claude plugin tag (introduced in v2.1.118) creates a release git tag with version validation. Marketplaces can source plugins from GitHub repositories (owner/repo format), git URLs, local paths, or remote URLs to marketplace.json files.
claude plugin enable and claude plugin disable toggle an installed plugin on or off without removing it. Both accept a <plugin> name (or <plugin>@<marketplace> to disambiguate) and a --scope of user, project, or local (default: user). Disabling at project scope writes the choice into .claude/settings.json so the whole team picks it up; user scope keeps it personal:
# Personal: turn off a noisy plugin just for you
claude plugin disable formatter@anthropics/claude-plugins
# Team: keep the plugin in settings but turn it off project-wide
claude plugin disable formatter --scope project
# Re-enable later without touching its install or version
claude plugin enable formatter --scope project
From the shell, claude plugin configure <plugin> (v2.1.285+) inspects and saves the values a plugin exposes through its manifest’s userConfig — the scriptable counterpart to the in-session /plugin configure dialog. With no flags it lists each declared option, labelled required or optional, sensitive where the manifest marks it, and set or not set; it never prints a saved value. To save values non-interactively, pipe a JSON object of single-line strings on stdin and add --values-stdin — handy for CI bootstrap and onboarding scripts; options you leave out keep their saved values. Pass the full <plugin>@<marketplace> id that claude plugin list shows — configure doesn’t accept a bare name — and an undeclared key or a value that fails its type check saves nothing and exits 1:
# List a plugin's options and which are set (prints no saved values)
claude plugin configure formatter@anthropics/claude-plugins
# Save values from a JSON object on stdin — no interactive prompts
echo '{"region":"us-east-1"}' \
| claude plugin configure formatter@anthropics/claude-plugins --values-stdin
claude plugin list reports every installed plugin with its version, scope, and status. Its --data-size [plugin] flag (v2.1.285+) measures each plugin’s saved data directory — the ~/.claude/plugins/data/<id>/ folder a plugin writes caches, state files, and databases to — so you can see what an uninstall would delete. It only takes effect alongside --json, which then adds a dataDirSize field (bytes and human) per plugin; pass a <plugin>@<marketplace> id to measure just that one:
# Measure every installed plugin's saved data directory
claude plugin list --json --data-size
# Measure a single plugin's saved data
claude plugin list --json --data-size formatter@anthropics/claude-plugins
The inline plugin pattern (source: 'settings' in v2.1.80+) lets you embed a plugin definition directly in a settings file without a separate repository. This is useful for small team-internal tools that don’t warrant a full git repository:
{
"pluginMarketplaces": [
{
"name": "internal-tools",
"source": "settings",
"plugins": [
{
"name": "code-standards",
"source": "./local-plugins/code-standards"
}
]
}
]
}