Claude · 24 · Part 11 · Hands-On Platform

Plugin Packaging: One Folder, Whole Setup

By now the bookshelf project has skills, two reviewer agents, a protective hook and an MCP server, each configured in its own place. A plugin puts all of it in one folder that Claude Code loads as a unit, with namespaced names so that nothing collides. This chapter builds that folder, validates it, loads it, and checks in real sessions that the skills, agents, MCP server and hook all arrive. It also corrects a path mistake in the plugin Chapter 12 only validated.

4
Component kinds packaged
2
Plugins validated
3
Real sessions
Information current as of: 2026-10-31. Real checks used Claude Code 2.1.316 with the haiku model on a copy of the bookshelf project. Read Chapters 18, 20, 22 and 23 for the pieces being packaged.
01
Why

When a Folder Beats Loose Files

Anthropic's guidance is plain: skills, subagents, hooks and MCP servers all work on their own, and you should keep that standalone setup while it serves one project or only you. Make a plugin when you want to share the setup with teammates, install it in several projects, or publish versioned releases (Source 1). A plugin is a directory of components plus a manifest, loaded as one unit.

One folder, four kinds of component and a manifest
bookshelf-kit/ .claude-plugin/plugin.jsonskills/ (3)agents/ (2)hooks/hooks.json.mcp.jsonmcp/feature-server.mjs the name: bookshelf-kit/bookshelf-kit:review-diffbookshelf-kit:sql-reviewerruns on Edit|Write callsplugin:bookshelf-kit:… names are prefixed, sotwo plugins cannot collide

One cost to know before you install anything: an enabled plugin is part of every session. For each skill and agent that Claude can invoke on its own, the name and description sit in Claude's context on every turn, and the plugin's MCP servers run alongside each session. The full text of a skill or agent loads only when it is used (Source 1). Package what you will use, not everything you own.

02
Layout

What Goes Where

The folder for the bookshelf plugin, as listed by find after I assembled it from the files of earlier chapters:

Actual output of find bookshelf-kit -type f · executed 2026-10-31
./bookshelf-kit/agents/api-reviewer.md ./bookshelf-kit/agents/sql-reviewer.md ./bookshelf-kit/.claude-plugin/plugin.json ./bookshelf-kit/hooks/hooks.json ./bookshelf-kit/hooks/protect-files.sh ./bookshelf-kit/mcp/feature-server.mjs ./bookshelf-kit/.mcp.json ./bookshelf-kit/skills/explain-endpoint/SKILL.md ./bookshelf-kit/skills/review-diff/checklist.md ./bookshelf-kit/skills/review-diff/SKILL.md ./bookshelf-kit/skills/verify-build/SKILL.md
LocationContents
.claude-plugin/plugin.jsonThe manifest. name is required, becomes the prefix on every skill and agent, and must not contain spaces
skills/One name/SKILL.md directory per skill (commands/ is the older flat form)
agents/One Markdown file per subagent
hooks/hooks.jsonA top-level hooks key whose value has the same shape as hooks in a settings file
.mcp.jsonMCP server definitions
The classic mistake. Only plugin.json goes inside .claude-plugin/. Skills, agents and hooks saved in there do not load. The plugin root is the folder that contains .claude-plugin/, not ~/.claude/ itself (Source 2).
.claude-plugin/plugin.json
{ "name": "bookshelf-kit", "version": "0.2.0", "description": "Review skills, two read-only reviewer agents, a file-protection hook and a feature-status MCP server for the bookshelf project", "author": { "name": "winaykumar" } }

version is optional; setting it keeps users on that version until you change it. author needs a name if present. The agents and skills are copies of the files from Chapters 20 and 19; nothing in them had to change except that they now carry the plugin's prefix.

03
The Wiring

Two Files That Need Paths

Skills and agents are self-contained. The hook and the MCP server are programs, so they need to find their scripts, and a plugin does not know where it will be installed. The answer is the variable ${CLAUDE_PLUGIN_ROOT}, which Claude Code sets to the plugin's installation directory (Source 1; the hooks and MCP pages list it for both uses).

hooks/hooks.json
{ "hooks": { "PreToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/protect-files.sh", "args": [], "onFailure": "block" } ] } ] } }
.mcp.json
{ "mcpServers": { "bookshelf-features": { "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/mcp/feature-server.mjs"], "env": { "FEATURE_FILE": "${CLAUDE_PROJECT_DIR}/feature_list.json" } } } }

Two details. The hook is the protect-files script of Chapter 22, run through the plugin variable. The server entry passes FEATURE_FILE as ${CLAUDE_PROJECT_DIR}/feature_list.json, so the plugin's server reads the current project's file, not one inside the plugin folder.

A mistake in Chapter 12's plugin. That chapter packaged a plugin named bookshelf-kit and checked only its manifest. Its .mcp.json started the server with a relative path, mcp/feature-server.mjs. A relative path resolves against the directory a process starts in, which for a server Claude Code launches is not the plugin's folder (my reading of the docs; I did not run the old file). The fix is the variable above. Chapter 12 never loaded the plugin, which is how the problem went unnoticed.
04
Validate

claude plugin validate, and What It Cannot See

claude plugin validate checks the manifest and the frontmatter of every skill, agent and command file, and exits 0 on Validation passed; --strict also fails on warnings (Source 2). Run on the new plugin:

Actual output of claude plugin validate ./bookshelf-kit · executed 2026-10-31
Validating plugin manifest: <examples-repo>/plugins/bookshelf-kit/.claude-plugin/plugin.json Plugin directory checks for <examples-repo>/plugins/bookshelf-kit. The plugin directory is the catalog that plugins are submitted to; this is a subset of what it runs. ✘ Found 1 problem the plugin directory refuses a submission for: ❯ LICENSE: License missing (LICENSE_MISSING). This plugin has no LICENSE file and plugin.json has no `license`. The directory requires one of them. Fix: Add a LICENSE file to the plugin folder, or set `license` (an SPDX id) in plugin.json. A LICENSE at the repository root also counts for a plugin in a sub-folder. ⚠ Found 1 plugin directory warning: ❯ .claude-plugin/plugin.json icon: No icon (ICON_MISSING). No icon found in the plugin. The validator looked for `icon` in plugin.json and for a file at .claude-plugin/icon.png or .svg, assets/icon.png or .svg, or a similar path. Fix: Add a square PNG to the plugin folder at .claude-plugin/icon.png, or set `icon` in plugin.json to the path of a square PNG or JPEG in the plugin: 512 to 2048 px on each side, under 2 MB. SVG and WebP are not accepted. That file may become the listing icon only once: the first time the plugin is saved or submitted in the developer portal. Adding or changing it later does not change the icon. About these checks: ❯ The repository root was taken to be <examples-repo>, the nearest parent folder with a .git folder or file. README.md was taken from it, because this plugin folder has none of its own. No license file that counts (a regular file with one of the file names the plugin directory accepts) was found there either. For a plugin submitted from a git repository, the repository root is the top folder of that repository, with only what is committed there, so check that the folder named above is that top folder. These plugin directory checks do not change the result below. To fail on problems the directory refuses a submission for, run this command with --profile directory. The directory also runs a safety scan and a policy review that this command does not. ✔ Validation passed
  • It passed. The last line is the verdict. Everything above it is about a different bar: the checks the plugin directory (the catalogue plugins are submitted to) applies. They say the plugin has no LICENSE file and no icon. They do not change the result unless you run with --profile directory. For a plugin you keep for yourself or your team, they are optional; I did not add a licence on your behalf, because that is your decision.
  • It did not catch the Chapter 12 path problem. Run on that older plugin, it also says passed, with a warning about a missing author:
Actual output of claude plugin validate on the Chapter 12 plugin · executed 2026-10-31
Validating plugin manifest: <ch12-kit>/.claude-plugin/plugin.json ⚠ Found 1 warning: ❯ author: No author information provided. Consider adding author details for plugin attribution Plugin directory checks for <ch12-kit>. The plugin directory is the catalog that plugins are submitted to; this is a subset of what it runs. ✘ Found 2 problems the plugin directory refuses a submission for: ❯ README.md: README missing (README_MISSING). This plugin has no README.md. People who install it and the directory's automated review read the README to learn what it does; the first line of a README in the plugin folder also stands in as the listing summary when plugin.json has no description. Fix: Add a README.md to the plugin folder describing what the plugin does, how to use it, and any setup it needs, in at least 40 words. A README at the repository root also counts for a plugin in a sub-folder. ❯ LICENSE: License missing (LICENSE_MISSING). This plugin has no LICENSE file and plugin.json has no `license`. The directory requires one of them. Fix: Add a LICENSE file to the plugin folder, or set `license` (an SPDX id) in plugin.json. A LICENSE at the repository root also counts for a plugin in a sub-folder. ⚠ Found 1 plugin directory warning: ❯ .claude-plugin/plugin.json icon: No icon (ICON_MISSING). No icon found in the plugin. The validator looked for `icon` in plugin.json and for a file at .claude-plugin/icon.png or .svg, assets/icon.png or .svg, or a similar path. Fix: Add a square PNG to the plugin folder at .claude-plugin/icon.png, or set `icon` in plugin.json to the path of a square PNG or JPEG in the plugin: 512 to 2048 px on each side, under 2 MB. SVG and WebP are not accepted. That file may become the listing icon only once: the first time the plugin is saved or submitted in the developer portal. Adding or changing it later does not change the icon. These plugin directory checks do not change the result below. To fail on problems the directory refuses a submission for, run this command with --profile directory. The directory also runs a safety scan and a policy review that this command does not. ✔ Validation passed with warnings

Validation checks structure. It does not start your MCP server or run your hook, so a green result means "well-formed", not "works". That gap is the reason for the next section.

05
Load and Prove

Does Everything Actually Arrive?

You do not need a marketplace to test a plugin. --plugin-dir loads a folder for one session without installing it, and /reload-plugins applies edits made during a session (Source 2):

claude --plugin-dir ./plugins/bookshelf-kit
Actual output of claude --plugin-dir ... plugin list · executed 2026-10-31
Session-only plugins (--plugin-dir / --plugin-url): ❯ bookshelf-kit@inline Version: 0.2.0 Path: <examples-repo>/plugins/bookshelf-kit Status: ✔ loaded

That shows the plugin loaded. To see what it contributed, I started a one-turn session and kept only the parts of the start-up event that mention bookshelf-kit. (The full event also lists everything else installed on the machine, so it is never published unfiltered; the filtered copy below is all that is kept.)

start-up event, filtered to bookshelf-kit · executed 2026-10-31
{ "plugins": [ "bookshelf-kit" ], "skills": [ "bookshelf-kit:explain-endpoint", "bookshelf-kit:review-diff", "bookshelf-kit:verify-build" ], "agents": [ "bookshelf-kit:api-reviewer", "bookshelf-kit:sql-reviewer" ], "mcp": [ { "name": "plugin:bookshelf-kit:bookshelf-features", "status": "connected" } ], "tool_names": [ "mcp__plugin_bookshelf-kit_bookshelf-features__feature_status", "mcp__plugin_bookshelf-kit_bookshelf-features__list_features" ] }
  • Skills and agents are prefixed: bookshelf-kit:review-diff, bookshelf-kit:sql-reviewer, and so on. They run as /bookshelf-kit:review-diff.
  • The MCP server connected, under the name plugin:bookshelf-kit:bookshelf-features. That means the ${CLAUDE_PLUGIN_ROOT} path in .mcp.json resolved and the server started.
  • Its tools carry a longer name: mcp__plugin_bookshelf-kit_bookshelf-features__list_features. The docs warn that a hook matcher written against the bare server name never matches a plugin-bundled server; this is why.

Listing is not using. Two real sessions, each with only the plugin loaded. First, the plugin's MCP tool:

Real run PL-01: prompt, every step, and the answer (Checked)
Prompt as sent
Which bookshelf features are failing? Use the feature MCP tool, not file reads.
Who did what, in order · executed 2026-10-31
#WhoEventContent (shortened)
1Main sessionmcp__plugin_bookshelf-kit_bookshelf-features__list_features call{}
2Main sessionTool result[{"type":"text","text":"#1 list books (passing)\n#2 get book by id with 404 (passing)\n#3 create book with validation (passing)\n#4 search by title (passing)\n#5 stats endpoint (passing)\n#6 paginatio
3Main sessionTextTwo features are failing: **#6 pagination for list** and **#7 delete book**. The other five pass: list books (#1), get book by id with 404 …
Final answer shown to you · 2 turns, 3.2 s, estimated $0.003, models claude-haiku-5-5
Two features are failing: **#6 pagination for list** and **#7 delete book**. The other five pass: list books (#1), get book by id with 404 (#2), create book with validation (#3), search by title (#4), and the stats endpoint (#5). I only pulled the list, not per-feature detail, so I don't know why #6 and #7 fail.
Author's check: Checked. The answer matches feature_list.json, and the tool name is the one the start-up event listed for the plugin's server.

The answer, #6 and #7 failing, matches feature_list.json in the project. Notice what is not in the table: the ToolSearch step that Chapter 23's session needed. Claude called the tool directly this time; I did not investigate why. Second, the plugin's hook, with Write allowed so that only the hook can refuse:

Real run PL-02: prompt, every step, and the answer (Checked)
Prompt as sent
Create a file named .env in the project root containing API_KEY=demo, then tell me exactly what happened.
Who did what, in order · executed 2026-10-31
#WhoEventContent (shortened)
1Main sessionBash call{"command": "ls -la .env 2>/dev/null; git check-ignore -v .env; git status --short", "description": "Check whether .env exists and whether it is git-i
2Main sessionTool resultPermission to use Bash has been denied because Claude Code is running in don't ask mode. IMPORTANT: You *may* attempt to accomplish this action using other tools that might naturally be used to accomp
3Main sessionTextBash was denied, so I'll create the file with the Write tool instead. If a `.env` already exists, Write will refuse to overwrite it without …
4Main sessionWrite call{"file_path": "<project>/.env", "content": "API_KEY=demo\n"}
5Main sessionTool resultPreToolUse:Write hook error: [<plugin-root>/hooks/protect-files.sh]: Blocked by bookshelf hook: <scratch>
6Main sessionTextI did not create `.env`. Nothing was written to the project. What happened, in order: 1. **Bash denied.** My first attempt was a read-only…
Final answer shown to you · 3 turns, 8.3 s, estimated $0.005, models claude-haiku-5-5
I did not create `.env`. Nothing was written to the project. What happened, in order: 1. **Bash denied.** My first attempt was a read-only check for an existing `.env` and whether git ignores it. The tool call was refused because Claude Code is running in "don't ask" permission mode. I did not try to get around that. 2. **Write blocked by a hook.** I then tried to create the file with the Write tool. A `PreToolUse` hook from the `bookshelf-kit` plugin (`hooks/protect-files.sh`) rejected the write. Its message says the path is protected and asks the user to change it. So the file is not there, and I have not verified whether an `.env` already exists. The recent commit "Ignore .env" suggests the project deliberately keeps `.env` out of version control, which may be why the hook is protecting it. I haven't confirmed that. To proceed, you could: - **Create it yourself:** `echo "API_KEY=demo" > .env` in the project root. - **Lift the protection:** tell me to proceed after you've changed the hook or its config for this file, and I'll retry the Write. Since the hook explicitly asks you to make this decision, I won't try other ways to write the file.
Author's check: Checked. No .env exists in the project afterwards (checked on disk). The hook message names the script inside the plugin folder (shortened to <plugin-root>), which shows ${CLAUDE_PLUGIN_ROOT} was expanded.

The write was refused by the plugin's hook, and the message names the script by its path inside the plugin folder (shortened here to <plugin-root>), which shows the variable was expanded. No .env file exists in the project afterwards. Three things work from one folder: names, a server and a guard.

06
Sharing · From the Docs, Not Run

Installing, Scopes and Marketplaces

Everything above used --plugin-dir. For a team, the documented routes are these, none of which I ran:

RouteHow
Send the folderGive people the directory or a .zip; they load it with --plugin-dir. No publishing needed
Your own marketplaceA repository with .claude-plugin/marketplace.json listing plugins; teammates add it once and install by name, and receive updates
Anthropic's directorySubmit for review; the validator's directory profile is the checklist

Installing asks for a scope: user (you, every project), project (everyone in the repository through the committed .claude/settings.json; each person still installs it on their own machine) or local (you, this repository). Manage plugins with /plugin or, in the shell, claude plugin disable to stop one without uninstalling it.

To turn existing .claude/ files into a plugin, copy skills, agents and commands to the plugin root and move the hooks object from your settings file into hooks/hooks.json. One gotcha from the docs: while the originals remain in .claude/, skills and agents do not collide (the plugin's carry a prefix), but hooks have no prefix, so a hook present in both places runs twice. Delete the originals once the plugin works.

A plugin runs as you. Its hooks and MCP servers execute with your privileges, whatever marketplace it came from. Review a plugin before you install it as you would a script you are about to run. Anthropic documents a security page for this, and organisations can allowlist or block marketplaces through managed settings (Source 1).
Not covered here. I did not install from a marketplace, publish, or run claude plugin eval, which the docs describe as running test cases with and without the plugin to check whether it changes Claude's behaviour. I also did not measure a plugin's context cost, which the docs say you can do before and after installing.
07
Summary

Key Takeaways, Exercises and Sources

∑ Plugins: what to remember
  • A plugin is a folder: a manifest plus skills, agents, hooks and MCP config at the plugin root, never inside .claude-plugin/.
  • Names gain a prefix: /plugin:skill, plugin:agent, and a longer MCP tool name.
  • Use ${CLAUDE_PLUGIN_ROOT} for any script the plugin ships. Relative paths are a trap.
  • Validate, then load, then use. Passing validation means well-formed, not working.
  • Package what you use. Every enabled plugin costs context and may start processes in every session.
Exercises

Exercise 1 Beginner

Scaffold a plugin with one skill, validate it, load it with --plugin-dir and run /your-plugin:skill. Then move the skill into .claude-plugin/ on purpose and watch it stop loading.

Learning outcome: internalise the layout rule by breaking it.

Exercise 2 Intermediate

Convert your own .claude/ folder into a plugin. Before deleting the originals, confirm a hook runs twice. Then delete them and confirm it runs once, using the audit-log hook from Chapter 22 to count.

Learning outcome: understand the duplicate-hook gotcha with evidence.

Exercise 3 Advanced

Put the plugin in a Git repository with a marketplace.json, add the marketplace on a second machine or user account, and install by name. Change the version, republish, and note what the second machine must do to update. Read the publishing page first.

Learning outcome: run the full distribution loop.

Sources (read on 2026-10-31; paraphrased)
  1. Anthropic, Plugins overview — what a plugin is, when to use one, what an enabled plugin adds to a session, marketplaces and scopes, security and trust. Read in full.
  2. Anthropic, Create a Claude Code plugin — layout, manifest, --plugin-dir, claude plugin validate, testing and debugging, converting an existing setup. Read in full; its linked reference pages (manifest reference, components, publishing, evals, security) were not.
  3. Anthropic, MCP and Hooks reference pages — ${CLAUDE_PLUGIN_ROOT} and the plugin-bundled MCP tool naming. Partly read; see Chapters 22 and 23.

Executed 2026-10-31: claude plugin validate on two plugins, claude plugin list with the plugin loaded, a filtered start-up event, and two short sessions, PL-01 and PL-02. Not run: marketplace install, publishing, claude plugin eval, cost measurement, the --profile directory check. The plugin is in the examples repository under plugins/bookshelf-kit/.