1> ## Documentation Index
2> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
3> Use this file to discover all available pages before exploring further.
4
5# Add components to a plugin
6
7> Add skills, hooks, MCP servers, and every other component type to a Claude Code plugin, with an example that validates for each.
8
9export const Piece = ({id, children}) => <div className="pe-piece" data-piece={id}>{children}</div>;
10
11export const PluginExplorer = ({children}) => {
12 const PIECES = [{
13 id: 'manifest',
14 name: 'Manifest',
15 path: '.claude-plugin/plugin.json',
16 required: "Required by Anthropic's directory",
17 lines: [{
18 depth: 0,
19 kind: 'folder',
20 text: '.claude-plugin/'
21 }, {
22 depth: 1,
23 kind: 'file',
24 text: 'plugin.json'
25 }],
26 href: '/en/plugins/manifest-reference#manifest-file',
27 linkText: 'Go to the manifest reference'
28 }, {
29 id: 'skills',
30 name: 'Skills',
31 path: 'skills/review/SKILL.md',
32 lines: [{
33 depth: 0,
34 kind: 'folder',
35 text: 'skills/'
36 }, {
37 depth: 1,
38 kind: 'folder',
39 text: 'review/'
40 }, {
41 depth: 2,
42 kind: 'file',
43 text: 'SKILL.md'
44 }],
45 href: '/en/plugins/components#skills',
46 linkText: 'Go to the Skills section'
47 }, {
48 id: 'commands',
49 name: 'Commands',
50 path: 'commands/about.md',
51 lines: [{
52 depth: 0,
53 kind: 'folder',
54 text: 'commands/'
55 }, {
56 depth: 1,
57 kind: 'file',
58 text: 'about.md'
59 }],
60 href: '/en/plugins/components#commands',
61 linkText: 'Go to the Commands section'
62 }, {
63 id: 'agents',
64 name: 'Agents',
65 path: 'agents/security-reviewer.md',
66 lines: [{
67 depth: 0,
68 kind: 'folder',
69 text: 'agents/'
70 }, {
71 depth: 1,
72 kind: 'file',
73 text: 'security-reviewer.md'
74 }],
75 href: '/en/plugins/components#agents',
76 linkText: 'Go to the Agents section'
77 }, {
78 id: 'hooks',
79 name: 'Hooks',
80 path: 'hooks/hooks.json',
81 lines: [{
82 depth: 0,
83 kind: 'folder',
84 text: 'hooks/'
85 }, {
86 depth: 1,
87 kind: 'file',
88 text: 'hooks.json'
89 }],
90 href: '/en/plugins/components#hooks',
91 linkText: 'Go to the Hooks section'
92 }, {
93 id: 'monitors',
94 name: 'Monitors',
95 path: 'monitors/monitors.json',
96 lines: [{
97 depth: 0,
98 kind: 'folder',
99 text: 'monitors/'
100 }, {
101 depth: 1,
102 kind: 'file',
103 text: 'monitors.json'
104 }],
105 href: '/en/plugins/components#monitors',
106 linkText: 'Go to the Monitors section'
107 }, {
108 id: 'output-styles',
109 name: 'Output styles',
110 path: 'output-styles/terse.md',
111 lines: [{
112 depth: 0,
113 kind: 'folder',
114 text: 'output-styles/'
115 }, {
116 depth: 1,
117 kind: 'file',
118 text: 'terse.md'
119 }],
120 href: '/en/plugins/components#themes-and-output-styles',
121 linkText: 'Go to the Themes and output styles section'
122 }, {
123 id: 'themes',
124 name: 'Themes',
125 path: 'themes/dracula.json',
126 lines: [{
127 depth: 0,
128 kind: 'folder',
129 text: 'themes/'
130 }, {
131 depth: 1,
132 kind: 'file',
133 text: 'dracula.json'
134 }],
135 href: '/en/plugins/components#themes-and-output-styles',
136 linkText: 'Go to the Themes and output styles section'
137 }, {
138 id: 'workflows',
139 name: 'Workflows',
140 path: 'workflows/audit-routes.js',
141 lines: [{
142 depth: 0,
143 kind: 'folder',
144 text: 'workflows/'
145 }, {
146 depth: 1,
147 kind: 'file',
148 text: 'audit-routes.js'
149 }],
150 href: '/en/workflows#distribute-a-workflow-in-a-plugin',
151 linkText: 'Go to Distribute a workflow in a plugin'
152 }, {
153 id: 'bin',
154 name: 'Executables',
155 path: 'bin/hello-plugin',
156 lines: [{
157 depth: 0,
158 kind: 'folder',
159 text: 'bin/'
160 }, {
161 depth: 1,
162 kind: 'file',
163 text: 'hello-plugin'
164 }],
165 href: '/en/plugins/components#executables',
166 linkText: 'Go to the Executables section'
167 }, {
168 id: 'scripts',
169 name: 'Scripts',
170 path: 'scripts/format.sh',
171 lines: [{
172 depth: 0,
173 kind: 'folder',
174 text: 'scripts/'
175 }, {
176 depth: 1,
177 kind: 'file',
178 text: 'format.sh'
179 }],
180 href: '/en/plugins/components#hooks',
181 linkText: 'Go to the Hooks section'
182 }, {
183 id: 'settings',
184 name: 'Default settings',
185 path: 'settings.json',
186 lines: [{
187 depth: 0,
188 kind: 'file',
189 text: 'settings.json'
190 }],
191 href: '/en/plugins/components#default-settings',
192 linkText: 'Go to the Default settings section'
193 }, {
194 id: 'mcp',
195 name: 'MCP servers',
196 path: '.mcp.json',
197 lines: [{
198 depth: 0,
199 kind: 'file',
200 text: '.mcp.json'
201 }],
202 href: '/en/plugins/components#mcp-servers',
203 linkText: 'Go to the MCP servers section'
204 }, {
205 id: 'lsp',
206 name: 'LSP servers',
207 path: '.lsp.json',
208 lines: [{
209 depth: 0,
210 kind: 'file',
211 text: '.lsp.json'
212 }],
213 href: '/en/plugins/components#lsp-servers',
214 linkText: 'Go to the LSP servers section'
215 }];
216 const [selectedId, setSelectedId] = useState('manifest');
217 const [isFullscreen, setIsFullscreen] = useState(false);
218 const rootRef = useRef(null);
219 useEffect(() => {
220 const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
221 document.addEventListener('fullscreenchange', onFsChange);
222 return () => document.removeEventListener('fullscreenchange', onFsChange);
223 }, []);
224 const toggleFullscreen = () => {
225 if (!rootRef.current) return;
226 if (document.fullscreenElement) document.exitFullscreen(); else rootRef.current.requestFullscreen().catch(() => {});
227 };
228 const selected = PIECES.find(p => p.id === selectedId) || PIECES[0];
229 const onTreeKeyDown = e => {
230 const keys = ['ArrowDown', 'ArrowUp', 'Home', 'End'];
231 if (keys.indexOf(e.key) === -1) return;
232 const i = PIECES.findIndex(p => p.id === selectedId);
233 let next = i;
234 if (e.key === 'ArrowDown') next = Math.min(PIECES.length - 1, i + 1);
235 if (e.key === 'ArrowUp') next = Math.max(0, i - 1);
236 if (e.key === 'Home') next = 0;
237 if (e.key === 'End') next = PIECES.length - 1;
238 e.preventDefault();
239 if (next === i) return;
240 const id = PIECES[next].id;
241 setSelectedId(id);
242 const el = document.getElementById('pe-node-' + id);
243 if (el) el.focus();
244 };
245 const FolderIcon = () => <svg className="pe-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" aria-hidden="true">
246 <path d="M1.5 4.5a1 1 0 0 1 1-1h3.2l1.3 1.5h6a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1h-10.5a1 1 0 0 1-1-1z" />
247 </svg>;
248 const FileIcon = () => <svg className="pe-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" aria-hidden="true">
249 <path d="M4 1.5h5.5L13 5v9.5H4z" />
250 <path d="M9.5 1.5V5H13" />
251 </svg>;
252 return <div ref={rootRef} className={isFullscreen ? 'pe-root pe-fullscreen not-prose' : 'pe-root not-prose'} data-selected={selected.id}>
253 <style>{`
254 .pe-root {
255 --pe-mono: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
256 --pe-accent: #D97757;
257 --pe-accent-text: #A8502F;
258 --pe-accent-bg: rgba(217,119,87,0.10);
259 --pe-bg: #FFFFFF;
260 --pe-surface: #FAFAF7;
261 --pe-hover: #F0EEE6;
262 --pe-border: #E8E6DC;
263 --pe-text: #141413;
264 --pe-text-2: #3D3D3A;
265 --pe-text-3: #5E5D59;
266 font-family: inherit;
267 background: var(--pe-bg);
268 color: var(--pe-text);
269 border: 1px solid var(--pe-border);
270 border-radius: 12px;
271 margin: 1.5rem 0;
272 overflow: hidden;
273 box-sizing: border-box;
274 }
275 .dark .pe-root {
276 --pe-accent-text: #EBA98F;
277 --pe-accent-bg: rgba(217,119,87,0.18);
278 --pe-bg: #1A1918;
279 --pe-surface: #232221;
280 --pe-hover: #2E2D2B;
281 --pe-border: #3A3936;
282 --pe-text: #F1EFE9;
283 --pe-text-2: #D6D4CA;
284 --pe-text-3: #B8B5AD;
285 }
286 .pe-root *, .pe-root *::before, .pe-root *::after { box-sizing: border-box; }
287 .pe-head { display: flex; align-items: flex-start; gap: 12px; padding: 18px 24px 16px; border-bottom: 1px solid var(--pe-border); }
288 .pe-head-text { flex: 1; min-width: 0; }
289 .pe-fs-btn { flex-shrink: 0; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; border: 1px solid var(--pe-border); border-radius: 6px; background: var(--pe-surface); color: var(--pe-text-2); font-size: 15px; line-height: 1; cursor: pointer; }
290 .pe-fs-btn:hover { background: var(--pe-hover); }
291 .pe-fs-btn:focus-visible { outline: 2px solid var(--pe-accent); outline-offset: 2px; }
292 .pe-fullscreen { border-radius: 0; height: 100vh; display: flex; flex-direction: column; overflow: auto; }
293 .pe-fullscreen .pe-body { flex: 1; }
294 .pe-title { font-size: 19px; font-weight: 600; line-height: 1.3; color: var(--pe-text); margin: 0; }
295 .pe-sub { font-size: 15px; line-height: 1.5; color: var(--pe-text-3); margin: 4px 0 0; }
296 .pe-sub code { font-family: var(--pe-mono); font-size: 0.88em; padding: 1px 5px; border-radius: 4px; background: var(--pe-surface); border: 1px solid var(--pe-border); }
297 .pe-body { display: flex; align-items: stretch; }
298 .pe-tree-pane { width: 270px; flex-shrink: 0; background: var(--pe-surface); border-right: 1px solid var(--pe-border); padding: 16px 0 12px; }
299 .pe-panel { flex: 1; min-width: 0; padding: 16px 24px 24px; }
300 .pe-caption { font-size: 13px; font-weight: 600; color: var(--pe-text-3); margin: 0 0 10px; }
301 .pe-tree-pane .pe-caption { padding: 0 16px; }
302 .pe-rootline { display: flex; align-items: center; gap: 7px; padding: 3px 16px; font-family: var(--pe-mono); font-size: 13.5px; color: var(--pe-text-3); }
303 .pe-node {
304 display: block; width: 100%; margin: 0; padding: 3px 16px 3px 30px; text-align: left; cursor: pointer;
305 background: transparent; color: var(--pe-text-2);
306 border: none; border-left: 3px solid transparent;
307 font-family: var(--pe-mono); font-size: 13.5px; line-height: 1.4;
308 }
309 .pe-node:hover { background: var(--pe-hover); }
310 .pe-node:focus-visible { outline: 2px solid var(--pe-accent); outline-offset: -2px; }
311 .pe-node[aria-pressed="true"] { background: var(--pe-accent-bg); border-left-color: var(--pe-accent); color: var(--pe-accent-text); font-weight: 600; }
312 .pe-line { display: flex; align-items: center; gap: 7px; padding: 2px 0; }
313 .pe-line-tree { flex-wrap: wrap; }
314 .pe-line-tree .pe-req { flex-basis: 100%; margin: 2px 0 0 22px; white-space: normal; width: fit-content; max-width: calc(100% - 22px); }
315 .pe-line span { overflow-wrap: anywhere; }
316 .pe-piece { display: none; font-size: 16px; line-height: 1.6; color: var(--pe-text-2); }
317 .pe-root[data-selected="manifest"] .pe-piece[data-piece="manifest"],
318 .pe-root[data-selected="skills"] .pe-piece[data-piece="skills"],
319 .pe-root[data-selected="commands"] .pe-piece[data-piece="commands"],
320 .pe-root[data-selected="agents"] .pe-piece[data-piece="agents"],
321 .pe-root[data-selected="hooks"] .pe-piece[data-piece="hooks"],
322 .pe-root[data-selected="monitors"] .pe-piece[data-piece="monitors"],
323 .pe-root[data-selected="output-styles"] .pe-piece[data-piece="output-styles"],
324 .pe-root[data-selected="themes"] .pe-piece[data-piece="themes"],
325 .pe-root[data-selected="workflows"] .pe-piece[data-piece="workflows"],
326 .pe-root[data-selected="bin"] .pe-piece[data-piece="bin"],
327 .pe-root[data-selected="scripts"] .pe-piece[data-piece="scripts"],
328 .pe-root[data-selected="settings"] .pe-piece[data-piece="settings"],
329 .pe-root[data-selected="mcp"] .pe-piece[data-piece="mcp"],
330 .pe-root[data-selected="lsp"] .pe-piece[data-piece="lsp"] { display: block; }
331 .pe-piece p { margin: 0 0 10px; }
332 .pe-piece p:last-child { margin-bottom: 0; }
333 .pe-piece code { font-family: var(--pe-mono); font-size: 0.88em; padding: 1px 5px; border-radius: 4px; background: var(--pe-surface); border: 1px solid var(--pe-border); }
334 .pe-piece .code-block { margin: 12px 0 0; }
335 .pe-piece pre code { padding: 0; border: none; background: none; }
336 .pe-piece a { color: var(--pe-accent-text); }
337 .pe-line-compact { display: none; }
338 .pe-icon { flex-shrink: 0; }
339 .pe-req { margin-left: 8px; padding: 0 6px; border-radius: 999px; font-size: 11px; line-height: 18px; letter-spacing: .02em; color: var(--pe-accent-text); border: 1px solid var(--pe-border); background: var(--pe-surface); white-space: nowrap; font-weight: 500; vertical-align: middle; }
340 .pe-name { font-size: 22px; font-weight: 600; line-height: 1.25; letter-spacing: -0.2px; color: var(--pe-text); margin: 0; }
341 .pe-path { font-family: var(--pe-mono); font-size: 13.5px; color: var(--pe-accent-text); margin: 4px 0 0; overflow-wrap: anywhere; }
342 .pe-block { margin: 20px 0 0; }
343 .pe-link {
344 display: inline-block; margin: 24px 0 0; padding: 8px 14px; border-radius: 8px;
345 font-size: 14.5px; font-weight: 600; text-decoration: none;
346 color: var(--pe-accent-text); background: var(--pe-accent-bg); border: 1px solid var(--pe-accent);
347 }
348 .pe-link:hover { filter: brightness(0.97); }
349 .pe-link:focus-visible { outline: 2px solid var(--pe-accent); outline-offset: 2px; }
350 @media (max-width: 700px) {
351 .pe-head { padding: 16px 16px 14px; }
352 .pe-body { flex-direction: column; }
353 .pe-tree-pane { width: 100%; border-right: none; border-bottom: 1px solid var(--pe-border); }
354 .pe-line-tree { display: none; }
355 .pe-line-compact { display: flex; }
356 .pe-panel { padding: 16px 16px 20px; }
357 }
358 `}</style>
359
360 <div className="pe-head">
361 <div className="pe-head-text">
362 <div className="pe-title">What goes in a plugin</div>
363 <div className="pe-sub">This example plugin, <code>my-plugin</code>, has one of every kind of component, each in its default location. Select a file or folder to read what it’s for and see what goes in it.</div>
364 </div>
365 <button type="button" className="pe-fs-btn" onClick={toggleFullscreen} aria-label={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'} title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
366 {isFullscreen ? '⤡' : '⛶'}
367 </button>
368 </div>
369
370 <div className="pe-body">
371 <div className="pe-tree-pane">
372 <div className="pe-caption" id="pe-tree-caption">Plugin directory</div>
373 <div role="group" aria-labelledby="pe-tree-caption" onKeyDown={onTreeKeyDown}>
374 <div className="pe-rootline"><FolderIcon /><span>my-plugin/</span></div>
375 {PIECES.map(p => <button key={p.id} id={'pe-node-' + p.id} type="button" className="pe-node" aria-pressed={p.id === selected.id} aria-label={p.name + ', ' + p.path} onClick={() => setSelectedId(p.id)}>
376 {p.lines.map((line, i) => <span key={i} className="pe-line pe-line-tree" style={{
377 paddingLeft: line.depth * 18 + 'px'
378 }}>
379 {line.kind === 'folder' ? <FolderIcon /> : <FileIcon />}
380 <span>{line.text}</span>
381 {p.required && i === p.lines.length - 1 ? <span className="pe-req">{p.required}</span> : null}
382 </span>)}
383 <span className="pe-line pe-line-compact">
384 <FileIcon />
385 <span>{p.path}</span>
386 {p.required ? <span className="pe-req">{p.required}</span> : null}
387 </span>
388 </button>)}
389 </div>
390 </div>
391
392 <div className="pe-panel" role="region" aria-labelledby="pe-panel-caption" aria-live="polite" aria-atomic="true">
393 <div className="pe-caption" id="pe-panel-caption">Selected piece</div>
394 <div className="pe-name">{selected.name}{selected.required ? <span className="pe-req">{selected.required}</span> : null}</div>
395 <div className="pe-path">{selected.path}</div>
396
397 <div className="pe-block">{children}</div>
398
399 <a className="pe-link" href={selected.href}>{selected.linkText}</a>
400 </div>
401 </div>
402 </div>;
403};
404
405A Claude Code plugin is built from components, such as skills, agents, hooks, and MCP servers. Each component has a default folder in the plugin, an optional manifest key in `.claude-plugin/plugin.json` that replaces or adds to that folder, and a name the user sees. For each key's full field table, see the [manifest reference](/docs/en/plugins/manifest-reference#fields).
406
407Use this page to add a component to a plugin that already loads.
408
409After you add a component, run `/reload-plugins` in a running session or start a new one so Claude Code loads it. To check the component's file before loading it, run [`claude plugin validate .`](/docs/en/plugins/cli-reference#plugin-validate) in your shell from the plugin directory.
410
411<Note>
412 These cases are covered on other pages:
413
414 * **Building your first plugin**: start with [Create a plugin](/docs/en/plugins/create)
415 * **Installing someone else's plugin**: see [Install plugins](/docs/en/plugins/install)
416 * **Your plugin's users are on claude.ai or in Cowork**: a different set of components loads there. See [Plugin structure and testing](https://claude.com/docs/plugins/build) and the [component support table](https://claude.com/docs/plugins/platform-support#compare-component-support-by-app)
417</Note>
418
419## Explore the plugin directory
420
421The explorer shows an example plugin, `my-plugin`, that has one of every kind of component in its default location:
422
423* A review skill and an `about` command
424* A security-review subagent
425* A hook that formats files after Claude edits them, and the `scripts/` folder it calls
426* A log monitor
427* An output style and a color theme
428* A route-audit workflow
429* A `hello-plugin` executable
430* Default settings
431* A local MCP server and a Go language server
432
433Each file is the smallest valid example of its format, there to show the shape rather than to be useful: a real skill or agent carries full instructions and often supporting files, and a real hook or monitor does real work. The sections after the explorer use the same files as their examples and link to fuller ones. Select a file or folder to read what it's for, see what goes in it, and find the section that covers it.
434
435<PluginExplorer>
436 <Piece id="manifest">
437 The [manifest](/docs/en/plugins/manifest-reference) is the `plugin.json` file in a plugin's `.claude-plugin/` directory. It contains the plugin's metadata and the `userConfig` values that Claude Code prompts the user for. Claude Code loads a plugin without one, but [Anthropic's directory](/docs/en/plugins/publish#submit-to-anthropics-directory) requires it. Inside the file, only `name` is required. In this one, `description` is the text users see for the plugin in `/plugin`, and `version` keeps users on that version until you change it:
438
439 ```json theme={null}
440 {
441 "name": "my-plugin",
442 "version": "1.0.0",
443 "description": "Review, formatting, and database tools for this team"
444 }
445 ```
446 </Piece>
447
448 <Piece id="skills">
449 A [skill](/docs/en/skills) is a `SKILL.md` file. Save each skill in its own directory under `skills/`. Claude reads every skill's `description`, and when what the user asks for matches it, such as asking Claude to review a pull request here, Claude loads the skill's instructions and follows them. The user can also run it directly as `/my-plugin:review`:
450
451 ```markdown theme={null}
452 ---
453 description: Reviews a pull request for style and test coverage. Use when asked to review code.
454 ---
455
456 Review the changed files. Report style problems first, then missing tests.
457 ```
458 </Piece>
459
460 <Piece id="commands">
461 A command is a single Markdown file the user runs by name. Commands are the older format: a skill runs by name the same way and can also carry supporting files in its own directory, so write new ones as skills and keep `commands/` for files you already have. This file becomes `/my-plugin:about` and takes the same frontmatter as a skill:
462
463 ```markdown theme={null}
464 ---
465 description: Summarize the repository
466 ---
467
468 Summarize what this repository does in three sentences.
469 ```
470 </Piece>
471
472 <Piece id="agents">
473 A [subagent](/docs/en/sub-agents) is a separate assistant, with its own instructions and its own context window, that Claude can delegate a task to and get a result back from. Each Markdown file under `agents/` defines one: the frontmatter names it and says when to use it, and the body is its system prompt. This one is named `my-plugin:security-reviewer`, and the user can invoke it with `@agent-my-plugin:security-reviewer`:
474
475 ```markdown theme={null}
476 ---
477 name: security-reviewer
478 description: Reviews code changes for security issues. Use after edits to authentication or input handling.
479 model: sonnet
480 ---
481
482 You are a security reviewer. Read the changed files and report injection, authentication, and secrets-handling risks.
483 ```
484 </Piece>
485
486 <Piece id="hooks">
487 A [hook](/docs/en/hooks-guide) runs something automatically at a point in Claude Code's lifecycle, such as after every file edit: a shell command, an HTTP request, an MCP tool call, a prompt to a model, or a subagent. Save the plugin's hooks in `hooks/hooks.json` at the plugin root. This one runs the plugin's `scripts/format.sh` after Claude writes or edits a file:
488
489 ```json theme={null}
490 {
491 "hooks": {
492 "PostToolUse": [
493 {
494 "matcher": "Write|Edit",
495 "hooks": [
496 {
497 "type": "command",
498 "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/format.sh\""
499 }
500 ]
501 }
502 ]
503 }
504 }
505 ```
506 </Piece>
507
508 <Piece id="monitors">
509 A monitor is a shell command that Claude Code starts in the background when the session starts and keeps running until it ends, using the [Monitor tool](/docs/en/tools-reference#monitor-tool). What it prints reaches Claude as notifications. A `when` field can instead start it the first time a named skill runs. This one tails an error log:
510
511 ```json theme={null}
512 [
513 {
514 "name": "error-log",
515 "command": "tail -F ./logs/error.log",
516 "description": "Application error log"
517 }
518 ]
519 ```
520 </Piece>
521
522 <Piece id="output-styles">
523 A plugin can include [output styles](/docs/en/output-styles), which change how Claude formats and phrases its replies. Save each output style as `output-styles/<name>.md`. This one appears in `/output-style` as `my-plugin:terse`:
524
525 ```markdown theme={null}
526 ---
527 name: terse
528 description: Answer in as few words as possible
529 keep-coding-instructions: true
530 ---
531
532 Keep every reply short. Skip preambles and summaries.
533 ```
534 </Piece>
535
536 <Piece id="themes">
537 A plugin can include [color themes](/docs/en/terminal-config#create-a-custom-theme) for the Claude Code interface. Save each theme as `themes/<slug>.json`. This one appears in `/theme` as `Dracula`, marked as from `my-plugin`:
538
539 ```json theme={null}
540 {
541 "name": "Dracula",
542 "base": "dark",
543 "overrides": {
544 "claude": "#bd93f9",
545 "error": "#ff5555"
546 }
547 }
548 ```
549 </Piece>
550
551 <Piece id="workflows">
552 The `workflows/` folder holds [workflow](/docs/en/workflows) `.js` files: a `meta` block, then a script body that orchestrates several subagents. This one runs as `/my-plugin:audit-routes`:
553
554 ```javascript theme={null}
555 export const meta = {
556 name: 'audit-routes',
557 description: 'Audit every route handler for missing auth checks',
558 }
559
560 const found = await agent('List every .ts file under src/routes/.', {
561 schema: { type: 'object', required: ['files'], properties: { files: { type: 'array', items: { type: 'string' } } } },
562 })
563
564 const audits = await pipeline(found.files, file =>
565 agent(`Audit ${file} for missing authentication checks.`, { label: file }),
566 )
567
568 return audits.filter(Boolean)
569 ```
570 </Piece>
571
572 <Piece id="bin">
573 `bin/` is how a plugin ships a command-line tool. While the plugin is enabled, Claude Code puts this folder on the `PATH` of the shell it runs commands in, so Claude, or a skill's instructions, can run the tool by name without the user installing anything. With this [executable](#executables) in place, `hello-plugin` is a command Claude can run:
574
575 ```bash theme={null}
576 #!/bin/bash
577 echo "hello from my-plugin"
578 ```
579 </Piece>
580
581 <Piece id="scripts">
582 The hook in `hooks/hooks.json` runs a script, and this folder is where the example keeps it. The name `scripts/` is a convention, not something Claude Code looks for: the hook points at the file by its path, `${CLAUDE_PLUGIN_ROOT}/scripts/format.sh`. A formatter script might look like this:
583
584 ```bash theme={null}
585 #!/bin/bash
586 npx prettier --write .
587 ```
588 </Piece>
589
590 <Piece id="settings">
591 A `settings.json` at the plugin root holds [settings](/docs/en/settings-reference) that apply while the plugin is enabled, so a plugin can change how the session behaves and not only add components. Only two keys take effect from a plugin, [`agent`](/docs/en/settings-reference#agent) and [`subagentStatusLine`](/docs/en/settings-reference#subagentstatusline); every other key is dropped. See [Default settings](#default-settings).
592
593 This one sets `agent`, which runs the session's main thread as the plugin's own `security-reviewer` agent, so that agent's system prompt, tool restrictions, and model apply to the whole session:
594
595 ```json theme={null}
596 {
597 "agent": "security-reviewer"
598 }
599 ```
600 </Piece>
601
602 <Piece id="mcp">
603 An [MCP server](/docs/en/mcp) gives Claude tools from an external system. Declare it in `.mcp.json` at the plugin root. This one starts a local server from a script inside the plugin, and appears in `/mcp` as `plugin:my-plugin:db`:
604
605 ```json theme={null}
606 {
607 "mcpServers": {
608 "db": {
609 "command": "node",
610 "args": ["${CLAUDE_PLUGIN_ROOT}/server.js"]
611 }
612 }
613 }
614 ```
615 </Piece>
616
617 <Piece id="lsp">
618 An LSP server gives Claude [diagnostics and code navigation](/docs/en/plugins/code-intelligence) for a language. Declare the server in `.lsp.json` at the plugin root. This one connects the Go language server for `.go` files:
619
620 ```json theme={null}
621 {
622 "gopls": {
623 "command": "gopls",
624 "args": ["serve"],
625 "extensionToLanguage": {
626 ".go": "go"
627 }
628 }
629 }
630 ```
631 </Piece>
632</PluginExplorer>
633
634## Add each kind of component
635
636Each section below covers one kind of component: where its files go in the plugin, an example that validates, what the user sees once the plugin loads, and the manifest key that changes the default location. Add the ones your plugin needs; none is required.
637
638### Skills
639
640A [skill](/docs/en/skills) is a `SKILL.md` file that Claude can load when its description matches the task. The user can also run it as a command. Save each skill in its own directory under `skills/`:
641
642```text theme={null}
643my-plugin/
644├── .claude-plugin/
645│ └── plugin.json
646└── skills/
647 └── review/
648 └── SKILL.md
649```
650
651Give the `SKILL.md` a `description` so Claude knows when to use it:
652
653```markdown skills/review/SKILL.md theme={null}
654---
655description: Reviews a pull request for style and test coverage. Use when asked to review code.
656---
657
658Review the changed files. Report style problems first, then missing tests.
659```
660
661After you load the plugin, `/my-plugin:review` runs the skill. The command name and who can invoke it follow these rules:
662
663* **Command name**: `/<plugin>:<directory>`, so `skills/review/SKILL.md` in `my-plugin` is `/my-plugin:review`. If you set `name` in the frontmatter, it replaces the last segment and the plugin prefix stays. See [how a skill gets its command name](/docs/en/skills#how-a-skill-gets-its-command-name)
664* **Who invokes it**: Claude, the user, or both, controlled by frontmatter. See [Control who invokes a skill](/docs/en/skills#control-who-invokes-a-skill)
665
666You can also place skills outside the default `skills/` directory:
667
668* **Additional directories**: list them in the `skills` manifest key. They add to the default `skills/` scan rather than replacing it, unlike `commands` and `agents`
669* **A single skill at the plugin root**: with no `skills/` directory and no `skills` manifest key, a `SKILL.md` at the plugin root loads as one skill. Set `name` in its frontmatter, because otherwise a marketplace install names the skill after its [cache directory](/docs/en/plugins/loading#find-plugins-on-disk) rather than your plugin
670
671To include instructions in a plugin, write them as a skill. Claude Code doesn't load a `CLAUDE.md` at the plugin root, and `claude plugin validate` warns `CLAUDE.md at the plugin root is not loaded as project context`.
672
673For frontmatter fields and supporting files, see [Skills](/docs/en/skills).
674
675### Commands
676
677A command is a single Markdown file the user runs by name, such as `/my-plugin:about`.
678
679<Note>
680 Commands are the older format, and [skills](#skills) supersede them for new work. A skill runs by name the same way, and it can also carry supporting files in its directory. Keep `commands/` for files you're moving over from `.claude/commands/`.
681</Note>
682
683Save a command at `commands/<file>.md` and it becomes `/<plugin>:<file>`. A subdirectory adds a segment, so `commands/db/migrate.md` is `/my-plugin:db:migrate`.
684
685Command files take the same frontmatter as skills.
686
687#### Define commands in the manifest
688
689You only need this if you want to keep command files somewhere other than `commands/`, or to define a short command inside `plugin.json` without a separate Markdown file. Set the `commands` manifest key, and Claude Code reads it instead of scanning `commands/`. The key takes a path, an array of paths, or an object that maps each command name to either a `source` file or inline `content`.
690
691This manifest defines `/my-plugin:about` inline, with no Markdown file:
692
693```json .claude-plugin/plugin.json theme={null}
694{
695 "name": "my-plugin",
696 "commands": {
697 "about": {
698 "content": "Summarize what this repository does in three sentences.",
699 "description": "Summarize the repository"
700 }
701 }
702}
703```
704
705Load the plugin and run `/my-plugin:about` in the session to confirm it loaded.
706
707For the full key syntax, see [`commands`](/docs/en/plugins/manifest-reference#commands).
708
709### Agents
710
711A [subagent](/docs/en/sub-agents) is a separate assistant, with its own instructions and context window, that Claude can delegate a task to. Each Markdown file under `agents/` defines one:
712
713```markdown agents/security-reviewer.md theme={null}
714---
715name: security-reviewer
716description: Reviews code changes for security issues. Use after edits to authentication or input handling.
717model: sonnet
718---
719
720You are a security reviewer. Read the changed files and report injection, authentication, and secrets-handling risks.
721```
722
723This agent is named `my-plugin:security-reviewer`, and the user can [invoke it explicitly](/docs/en/sub-agents#invoke-subagents-explicitly) with `@agent-my-plugin:security-reviewer`. The name form is `<plugin>:<name>`, where `<name>` comes from the frontmatter, or from the file name when there is none.
724
725The `agents` manifest key replaces the `agents/` scan.
726
727#### Organize agents in subfolders
728
729You can put plugin agent files in subfolders of `agents/`. Claude Code [loads them recursively](/docs/en/sub-agents#choose-the-subagent-scope) and joins the plugin name, each subfolder name, and the file name with colons to form the agent's scoped name. For example, `agents/review/security.md` in a plugin named `my-plugin` loads as `my-plugin:review:security`. Two settings change that name:
730
731* Frontmatter `name`: it replaces only the file name, so `name: audit` in `agents/review/security.md` loads as `my-plugin:review:audit`
732* Manifest [`agents`](/docs/en/plugins/manifest-reference#fields) field: a file you list there loads without subfolder names, so `"agents": "./custom/review/security.md"` loads as `my-plugin:security`
733
734#### Frontmatter fields in plugin agents
735
736A plugin agent's frontmatter follows these rules:
737
738* **Supported fields**: `name`, `description`, `model`, `effort`, `maxTurns`, `tools`, `disallowedTools`, `skills`, `memory`, `background`, `omitClaudeMd`, `isolation`, `color`, and the `cacheTtl` key of `experimental`. The only valid `isolation` value is `"worktree"`. See [supported frontmatter fields](/docs/en/sub-agents#supported-frontmatter-fields) for what each one does
739* **Ignored fields**: `permissionMode`, `hooks`, `mcpServers`, and `initialPrompt`. An agent file can't add hooks or MCP servers on its own, so add those as plugin [hooks](#hooks) and [MCP servers](#mcp-servers) instead
740* **Frontmatter that doesn't parse**: the agent still loads with every field ignored. It's named after the file, and its description reads `Agent from my-plugin plugin`. Run [`claude plugin validate`](/docs/en/plugins/cli-reference#plugin-validate) in your shell to find these files
741
742For what each field does and the precedence rules, see [Subagents](/docs/en/sub-agents#supported-frontmatter-fields).
743
744### Hooks
745
746A [hook](/docs/en/hooks-guide) runs something automatically at a point in Claude Code's lifecycle, such as after every file edit: a shell command, an HTTP request, an MCP tool call, a prompt to a model, or a subagent. Save the plugin's hooks in `hooks/hooks.json` at the plugin root, under a top-level `"hooks"` key, in the same shape as the `hooks` object in `settings.json`. That lets you copy an existing settings hook in unchanged.
747
748This hook runs a bundled script after every `Write` or `Edit`:
749
750```json hooks/hooks.json theme={null}
751{
752 "hooks": {
753 "PostToolUse": [
754 {
755 "matcher": "Write|Edit",
756 "hooks": [
757 {
758 "type": "command",
759 "command": "\"${CLAUDE_PLUGIN_ROOT}/scripts/format.sh\""
760 }
761 ]
762 }
763 ]
764 }
765}
766```
767
768Save the script at `scripts/format.sh` and make it executable.
769
770Load the plugin and ask Claude to edit a file. A `PostToolUse` hook that exits 0 shows nothing in the transcript, so confirm it ran with [debug logging](/docs/en/hooks#debug-hooks) or by what the script itself changes.
771
772Hooks in `hooks/hooks.json` and in the `hooks` manifest key both load. For every event and its payload, see [Hook events](/docs/en/hooks#hook-events).
773
774#### When plugin hooks fire
775
776A plugin's hooks don't wait for one of the plugin's skills or commands to be used. Claude Code registers them when a session loads the plugin, and they fire on their events from then on. To limit when a hook runs, narrow its `matcher`.
777
778If a hook never fires, see [hooks that don't fire](/docs/en/plugins/troubleshooting#failed-to-load-hooks-from-and-hooks-that-dont-fire).
779
780#### Environment, quoting, and matching MCP tools
781
782The hook's environment, the quoting of `${CLAUDE_PLUGIN_ROOT}`, and matchers for the plugin's own MCP tools work as follows:
783
784* **Environment**: every hook process receives `CLAUDE_PLUGIN_ROOT` and `CLAUDE_PLUGIN_DATA` in its environment, plus `CLAUDE_PLUGIN_OPTION_<KEY>` for each [user configuration](#user-configuration) value, so your script can read them from there
785* **Quoting**: when `command` has no `args`, it runs through a shell, so wrap the `${CLAUDE_PLUGIN_ROOT}` path in double quotes, as the `hooks/hooks.json` example under [Hooks](#hooks) does, to keep the expanded path one shell word. When you pass `args` instead, each element is passed as one argument with no shell and needs no quoting. See [exec form and shell form](/docs/en/hooks#exec-form-and-shell-form)
786* **Matching the plugin's own MCP tools**: a tool from an [MCP server this plugin declares](#mcp-servers) is named `mcp__plugin_<plugin>_<server>__<tool>`, so write that full name in the matcher. A matcher on the server name alone never fires. See [Match MCP tools](/docs/en/hooks#match-mcp-tools)
787
788### MCP servers
789
790An MCP server gives Claude tools from an external system. Declare it in `.mcp.json` at the plugin root, in the same shape as a [project `.mcp.json`](/docs/en/mcp#project-scope). This `.mcp.json` declares one server named `db`:
791
792```json .mcp.json theme={null}
793{
794 "mcpServers": {
795 "db": {
796 "command": "node",
797 "args": ["${CLAUDE_PLUGIN_ROOT}/server.js"]
798 }
799 }
800}
801```
802
803You can also omit the `mcpServers` wrapper and put `db` at the top level of the file.
804
805Load the plugin and run `/mcp` to confirm the server appears as `plugin:my-plugin:db`.
806
807`claude plugin validate` checks `.mcp.json` and reports a server entry that Claude Code would drop at load time as an error. Requires Claude Code v2.1.281 or later.
808
809For where a bad entry shows up at load time, see [MCP servers that don't start](/docs/en/plugins/troubleshooting#invalid-mcp-server-config-for-and-mcp-servers-that-dont-start).
810
811The `mcpServers` manifest key takes an inline server map, a path to a JSON file, or an array of those. When a manifest server has the same name as one in `.mcp.json`, the manifest server replaces it.
812
813#### Reach users on claude.ai and Cowork
814
815A local stdio server, such as the `db` server under [MCP servers](#mcp-servers), runs in Claude Code and in a Cowork session that runs on your machine in the Claude Desktop app, but not on claude.ai. To reach users there too, reference a remote server by its `https://` URL, which claude.ai and Cowork offer to the user as a connector, as [Bundle an MCP connector with its skill](https://claude.com/docs/plugins/build#bundle-an-mcp-connector-with-its-skill) shows.
816
817#### Server names, tool names, and reloads
818
819The server's names, variable substitution, and reload behavior follow these rules:
820
821* **Server name**: `plugin:<plugin>:<server>`, so the `db` server in `my-plugin` is `plugin:my-plugin:db` in `/mcp`. Use the same form to name the server in an [`mcp_tool` hook](/docs/en/hooks#mcp-tool-hook-fields)
822* **Tool names**: `mcp__plugin_<plugin>_<server>__<tool>`, so a `query` tool on that `db` server is `mcp__plugin_my-plugin_db__query`. That is the name to use in [permission rules](/docs/en/permissions) and [hook matchers](#hooks)
823* **Substitution**: `${CLAUDE_PLUGIN_ROOT}` and the other [path variables](#path-variables-and-persistent-data) are substituted in `command`, `args`, and `env`. No quoting is needed in `args`, because each element is passed as one argument
824* **Reload**: when the user runs `/reload-plugins` and [the reload applies](/docs/en/plugins/cli-reference#reloads-that-change-mcp-tools), a server whose configuration is unchanged keeps its connection. A server whose configuration changed reconnects, and one you removed disconnects
825
826#### Include a packaged MCPB server
827
828The `mcpServers` key also accepts a packaged server as an [MCPB file](https://github.com/modelcontextprotocol/mcpb), whose extension is `.mcpb` or the older `.dxt`. Point the key at the file, as a path inside the plugin or an `https://` URL:
829
830```json .claude-plugin/plugin.json theme={null}
831{
832 "name": "my-plugin",
833 "mcpServers": "./servers/db.mcpb"
834}
835```
836
837The server takes its name from the `name` in the bundle's manifest.
838
839For transports and authentication, see [MCP](/docs/en/mcp#plugin-provided-mcp-servers).
840
841### LSP servers
842
843An LSP server gives Claude diagnostics and code navigation for a language. If an [official code intelligence plugin](/docs/en/plugins/code-intelligence) already covers your language, install that instead of writing one. Otherwise declare the server in `.lsp.json` at the plugin root:
844
845```json .lsp.json theme={null}
846{
847 "gopls": {
848 "command": "gopls",
849 "args": ["serve"],
850 "extensionToLanguage": {
851 ".go": "go"
852 }
853 }
854}
855```
856
857The file maps each server name directly to its configuration, with no wrapper object around the map. `command` is the binary's name, with its arguments in `args`. `extensionToLanguage` needs at least one extension, each starting with `.`.
858
859`claude plugin validate` doesn't read this file. When any entry is invalid, the whole file is skipped at load and `Invalid LSP server config for ".lsp.json"` appears in the `/plugin` **Errors** tab.
860
861Your plugin configures the connection but doesn't install the server binary, and each file extension gets one server:
862
863* **Missing binary**: Claude Code starts `command` by name from the user's `PATH`. When the binary isn't there, the server fails to start and `claude --debug` logs `LSP server <name> failed to start`
864* **Extension conflicts**: when two enabled servers claim the same extension, the first registered handles those files and the other isn't used for them, whether the servers come from one plugin or two. The `/plugin` **Errors** tab shows the warning `LSP server "<name>" is not used for <ext> files`
865
866The `lspServers` manifest key takes the same map inline, a path to a JSON file, or an array of those, and its servers add to the ones in `.lsp.json`. When a manifest server has the same name as one in `.lsp.json`, the manifest server replaces it.
867
868For `transport`, timeouts, restarts, and the other fields, see [`lspServers`](/docs/en/plugins/manifest-reference#lspservers).
869
870Send log output to stderr, not stdout. Claude Code reads a server's stdout as protocol messages only, and accepts message headers up to 64 KiB and a message body up to 32 MiB.
871
872Claude Code disconnects a server that exceeds either limit or writes non-protocol output to stdout, and counts the disconnect as a crash for `restartOnCrash` and `maxRestarts`. When you run with `--debug`, Claude Code writes an error naming the cause to the debug log.
873
874### Executables
875
876Files in `bin/` at the plugin root are on the `PATH` of the Bash tool's shell while the plugin is enabled, so Claude can run them as bare commands. Add an executable script:
877
878```bash bin/hello-plugin theme={null}
879#!/bin/bash
880echo "hello from my-plugin"
881```
882
883Make it executable with `chmod +x bin/hello-plugin` and load the plugin. When you ask Claude to run `hello-plugin`, the Bash tool result shows the script's output.
884
885Plugin `bin/` directories come after the user's own `PATH` entries, so a plugin can't shadow `git`, `ls`, or another system command.
886
887claude.ai and Cowork don't install a plugin that has a top-level `bin/` directory, including one you [distribute through claude.ai organization settings](https://claude.com/docs/plugins/org-sync#keep-executables-out-of-the-top-level-bin-directory).
888
889### Default settings
890
891To set defaults that apply while the plugin is enabled, add a `settings.json` at the plugin root, or put the same object inline in the `settings` manifest key. Two keys take effect, `agent` and `subagentStatusLine`, and every other key is dropped.
892
893Set `agent` to run one of the plugin's own agents as the main thread:
894
895```json settings.json theme={null}
896{
897 "agent": "security-reviewer"
898}
899```
900
901Load the plugin and start a session. Claude then answers in the main conversation with the `security-reviewer` agent's system prompt and model.
902
903For everything the key controls, see the [`agent` setting](/docs/en/settings-reference#agent).
904
905When the same key is set in more than one place, these rules decide which value applies:
906
907* **File over manifest**: when both exist and `settings.json` sets at least one supported key, `settings.json` applies and the manifest's `settings` is ignored
908* **User settings over plugin defaults**: across settings sources, plugin defaults are the lowest layer, so a user's own `agent` in `~/.claude/settings.json` overrides yours
909* **Two plugins set the same key**: the value from the plugin loaded last applies, and `claude --debug` logs `overrides setting`
910
911For the `subagentStatusLine` shape, see [subagent status lines](/docs/en/statusline#subagent-status-lines).
912
913### Themes and output styles
914
915A plugin can include color themes and output styles. Both appear in the same pickers as the user's own. For either one, setting the manifest key replaces the folder scan.
916
917| Component | Save as | Format | Appears in | Manifest key |
918| :----------- | :------------------------ | :-------------------------------------------------------------------------------------------------------------------------- | :------------------------------------ | :-------------------- |
919| Theme | `themes/<slug>.json` | The [custom theme file](/docs/en/terminal-config#create-a-custom-theme) format users write in `~/.claude/themes/` | `/theme`, under the file's `name` | `experimental.themes` |
920| Output style | `output-styles/<name>.md` | The [custom output style](/docs/en/output-styles#create-a-custom-output-style) format, with `name` and `description` frontmatter | `/output-style`, as `<plugin>:<name>` | `outputStyles` |
921
922Plugin themes are read-only, so when a user edits one in `/theme`, the edit is saved as a copy in their own themes directory.
923
924This theme recolors the prompt accent and error text on the dark preset:
925
926```json themes/dracula.json theme={null}
927{
928 "name": "Dracula",
929 "base": "dark",
930 "overrides": {
931 "claude": "#bd93f9",
932 "error": "#ff5555"
933 }
934}
935```
936
937### Channels
938
939A [channel](/docs/en/channels) lets an outside system such as a chat app send messages into a session. In a plugin, a channel is one of the MCP servers plus a `channels` entry that binds to it and can prompt for its own configuration. This manifest binds a channel to a `telegram` server and asks for a bot token:
940
941```json .claude-plugin/plugin.json theme={null}
942{
943 "name": "my-plugin",
944 "mcpServers": {
945 "telegram": {
946 "command": "node",
947 "args": ["${CLAUDE_PLUGIN_ROOT}/server.js"],
948 "env": { "BOT_TOKEN": "${user_config.bot_token}" }
949 }
950 },
951 "channels": [
952 {
953 "server": "telegram",
954 "userConfig": {
955 "bot_token": {
956 "type": "string",
957 "title": "Bot token",
958 "description": "Telegram bot token",
959 "sensitive": true
960 }
961 }
962 }
963 ]
964}
965```
966
967`server` must match a key in `mcpServers`. The per-channel `userConfig` takes the same shape as the [top-level `userConfig` key](#user-configuration).
968
969For what the server must implement and how users enable a channel plugin, see [Package as a plugin](/docs/en/channels-reference#package-as-a-plugin) in the channels reference. For the field table, see [`channels`](/docs/en/plugins/manifest-reference#channels).
970
971### Monitors
972
973A monitor is a shell command that runs in the background for the whole session. What it prints reaches Claude as notifications, so Claude can react to a log or a status change without being asked to watch it. Save the entries in `monitors/monitors.json`:
974
975```json monitors/monitors.json theme={null}
976[
977 {
978 "name": "error-log",
979 "command": "tail -F ./logs/error.log",
980 "description": "Application error log"
981 }
982]
983```
984
985The command runs in a shell, in the working directory the session started in.
986
987A monitor's command is limited in where it starts and what it can reference:
988
989* **Interactive sessions only**: plugin monitors start in an interactive session and never in non-interactive mode with the `-p` flag. They also start only where the [Monitor tool](/docs/en/tools-reference#monitor-tool) is available
990* **No user configuration**: `command` gets the [path variables](#path-variables-and-persistent-data) and `${ENV_VAR}` from the environment, but never `${user_config.*}`. A monitor that references one doesn't start, and monitor processes don't receive `CLAUDE_PLUGIN_OPTION_<KEY>` either
991* **Disabling mid-session**: if you disable a plugin mid-session, Claude Code doesn't stop monitors that are already running. They stop when the session ends
992
993The `experimental.monitors` manifest key takes the same array inline or a path to a JSON file, and is read instead of `monitors/monitors.json`.
994
995For the `when` trigger and the other fields, see [`monitors`](/docs/en/plugins/manifest-reference#monitors).
996
997<h2 id="user-configuration">
998 Ask the user for configuration values
999</h2>
1000
1001Declare the values your plugin needs from the user in the `userConfig` manifest key, so users don't edit `settings.json` themselves. Each option appears in a dialog with its `title` as the label and its `description` beneath it.
1002
1003Set `"sensitive": true` for a token or password. The dialog then masks the input, and the value is stored in secure storage rather than `settings.json`.
1004
1005This manifest asks for an endpoint and a token:
1006
1007```json .claude-plugin/plugin.json theme={null}
1008{
1009 "name": "my-plugin",
1010 "userConfig": {
1011 "api_url": {
1012 "type": "string",
1013 "title": "API URL",
1014 "description": "Base URL of your team's API"
1015 },
1016 "api_token": {
1017 "type": "string",
1018 "title": "API token",
1019 "description": "Token for your team's API",
1020 "sensitive": true
1021 }
1022 }
1023}
1024```
1025
1026### When the configuration dialog appears
1027
1028The dialog appears only in the interactive `/plugin` interface. It opens for any option that isn't set yet when the user does any of the following:
1029
1030* Installs the plugin in `/plugin`
1031* Runs `/plugin install <plugin>@<marketplace>` inside a session
1032* Enables the plugin from the **Installed** tab in `/plugin`
1033
1034To open the same dialog at any time, the user runs `/plugin configure <plugin>@<marketplace>`.
1035
1036The `claude plugin install` shell command never prompts for `userConfig` values. To set values from the shell, pass each one as `--config KEY=VALUE`. When options remain unset, the command prints a `userConfig options not yet set` line that names both ways to set them. [The `userConfig` dialog never appears](/docs/en/plugins/troubleshooting#the-userconfig-dialog-never-appears) quotes the line.
1037
1038For the option fields, where each value is stored, how a component references a saved value, and which fields reject `${user_config.*}`, see [User configuration](/docs/en/plugins/manifest-reference#user-configuration).
1039
1040<h2 id="path-variables-and-persistent-data">
1041 Reference plugin paths and store data
1042</h2>
1043
1044You don't know where your plugin will be installed, so refer to its files and data through these variables rather than fixed paths. They're substituted in skill, command, and agent content, in hook and monitor commands, and in MCP and LSP server configurations. They're also exported to hook, MCP, and LSP processes:
1045
1046* **`${CLAUDE_PLUGIN_ROOT}`**: the plugin's install directory. Each version has its own [cache directory](/docs/en/plugins/loading#find-plugins-on-disk), so the path changes when the plugin updates. Don't write state there
1047* **`${CLAUDE_PLUGIN_DATA}`**: a directory that survives updates, for `node_modules`, virtual environments, and caches. It resolves to `~/.claude/plugins/data/<id>/` and is created when first referenced
1048* **`${CLAUDE_PROJECT_DIR}`**: the project root, the same value hooks receive
1049
1050In the data directory path, `<id>` is the plugin identifier with every character other than letters, digits, `_`, and `-` replaced by `-`, so `my-plugin@my-marketplace` becomes `my-plugin-my-marketplace`.
1051
1052On Windows, the substituted paths use forward slashes so a shell doesn't read backslashes as escapes.
1053
1054### Install dependencies into the data directory
1055
1056For a marketplace-installed plugin, Claude Code installs eligible [Node.js package dependencies](/docs/en/plugins/loading#node-js-package-dependencies) automatically when it caches the plugin, so you may not need to install them yourself. When you do, this `SessionStart` hook installs `node_modules` into `${CLAUDE_PLUGIN_DATA}` on first run and again after an update changes `package.json`:
1057
1058```json hooks/hooks.json theme={null}
1059{
1060 "hooks": {
1061 "SessionStart": [
1062 {
1063 "hooks": [
1064 {
1065 "type": "command",
1066 "command": "diff -q \"${CLAUDE_PLUGIN_ROOT}/package.json\" \"${CLAUDE_PLUGIN_DATA}/package.json\" >/dev/null 2>&1 || (cd \"${CLAUDE_PLUGIN_DATA}\" && cp \"${CLAUDE_PLUGIN_ROOT}/package.json\" . && npm install) || rm -f \"${CLAUDE_PLUGIN_DATA}/package.json\""
1067 }
1068 ]
1069 }
1070 ]
1071 }
1072}
1073```
1074
1075After the first session, `~/.claude/plugins/data/<id>/node_modules` exists. An MCP server can then set `NODE_PATH` to `${CLAUDE_PLUGIN_DATA}/node_modules` in its `env`. For which fields substitute which variable, see [Environment variables](/docs/en/plugins/manifest-reference#environment-variables).
1076
1077## Next steps
1078
1079* [Plugin manifest reference](/docs/en/plugins/manifest-reference): `plugin.json` fields, path rules, and the standard layout
1080* [Test plugins with evals](/docs/en/plugin-evals): check that the components you added change Claude's behavior the way you intend
1081* [Publish and distribute a plugin](/docs/en/plugins/publish): version the plugin and put it in a marketplace
1082* [Troubleshoot plugins](/docs/en/plugins/troubleshooting): what to do when a component doesn't load or a hook doesn't fire