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.
haiku model on a copy of the bookshelf project. Read Chapters 18, 20, 22 and 23 for the pieces being packaged.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 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.
What Goes Where
The folder for the bookshelf plugin, as listed by find after I assembled it from the files of earlier chapters:
| Location | Contents |
|---|---|
.claude-plugin/plugin.json | The 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.json | A top-level hooks key whose value has the same shape as hooks in a settings file |
.mcp.json | MCP server definitions |
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).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.
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).
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.
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.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:
- 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:
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.
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):
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.)
- 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.jsonresolved 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)
| # | Who | Event | Content (shortened) |
|---|---|---|---|
| 1 | Main session | mcp__plugin_bookshelf-kit_bookshelf-features__list_features call | {} |
| 2 | Main session | Tool 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 |
| 3 | Main session | Text | 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 … |
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)
| # | Who | Event | Content (shortened) |
|---|---|---|---|
| 1 | Main session | Bash 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 |
| 2 | Main session | Tool result | Permission 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 |
| 3 | Main session | Text | Bash 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 … |
| 4 | Main session | Write call | {"file_path": "<project>/.env", "content": "API_KEY=demo\n"} |
| 5 | Main session | Tool result | PreToolUse:Write hook error: [<plugin-root>/hooks/protect-files.sh]: Blocked by bookshelf hook: <scratch> |
| 6 | Main session | Text | 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… |
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.
Installing, Scopes and Marketplaces
Everything above used --plugin-dir. For a team, the documented routes are these, none of which I ran:
| Route | How |
|---|---|
| Send the folder | Give people the directory or a .zip; they load it with --plugin-dir. No publishing needed |
| Your own marketplace | A repository with .claude-plugin/marketplace.json listing plugins; teammates add it once and install by name, and receive updates |
| Anthropic's directory | Submit 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.
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.Key Takeaways, Exercises and Sources
- 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.
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.
- 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.
- 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. - 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/.