Packaging Claude Code Plugins: Bundle Skills, Hooks, Subagents, and MCP Servers
Disclosure: jsmanifest has a financial interest in NoCensor AI and may earn from signups through links in this article.
Most plugin packaging failures stem from treating skills as standalone units. Learn the correct architecture for bundling capabilities, hooks, subagents, and MCP servers into production-ready Claude Code plugins.
Correction (2026-10-28): an earlier version of this post placed
plugin.jsonat the plugin's root and described skills, hooks, and subagents as TypeScript modules with an"entry"file, aSkillContext/HookContextAPI, a"capabilities"manifest wrapper, and a"runtime"/npm-style"dependencies"block. None of that exists. The manifest lives at.claude-plugin/plugin.jsonwith fields such asname,version,author(an object), andmcpServers; a skill is a directory with aSKILL.mdfile; a hook is defined inhooks/hooks.jsonagainst real events likePreToolUseandStop, running a shell command that reads JSON on stdin; and a subagent is a Markdown file underagents/with YAML frontmatter. The post's publishing steps (code signing, aclaude plugin publishcommand) were also invented; publishing means adding a.claude-plugin/marketplace.jsonfile and pushing it, or submitting through the claude.ai developer portal. All checked against the official docs.
Packaging Claude Code Plugins: Bundle Skills, Hooks, Subagents, and MCP Servers
Most plugin packaging failures stem from treating skills as isolated functions. Teams create individual capabilities—a linter, a deployment script, a code analyzer—and distribute them separately. This approach creates installation sprawl, version conflicts, and integration headaches.
The correct pattern is the plugin: a single installable unit that bundles related skills, lifecycle hooks, subagents, and MCP servers together. One install gives you a complete toolkit.
What Claude Code Plugins Are and Why They Matter in 2026
A Claude Code plugin is a packaging layer that wraps multiple agent capabilities into one distributable unit. Unlike skills (which execute specific tasks) or extensions (which modify editor behavior), plugins solve the integration problem.
When developers build agentic workflows, they need coherent capability groups. A "Deploy to AWS" plugin might bundle:
- A skill that validates infrastructure configurations
- A lifecycle hook that runs pre-deployment checks
- A subagent that monitors deployment progress
- An MCP server that connects to AWS APIs
Without plugins, installing those four components separately creates dependency hell. With plugins, one claude plugin install aws-deploy command activates the entire stack.
The market shift in 2026 reflects this. Plugin marketplaces now dominate AI agent ecosystems because they solve distribution at the right abstraction level. Developers who understand plugin architecture ship faster and maintain cleaner codebases.
The Anatomy of a Plugin: Skills, Hooks, Subagents, and MCP Servers
A plugin's structure mirrors application packaging. The core components form a hierarchy where each layer serves a distinct purpose.
Skills are the execution primitives. They perform discrete operations: parse a file, call an API, transform data. Skills receive structured inputs and return typed outputs. When a plugin bundles three skills, those skills share context and configuration.
Hooks intercept the agent lifecycle at real, named points. A PreToolUse hook can validate prerequisites before a tool call goes through, and a Stop hook can trigger cleanup when Claude finishes responding. Hooks let plugins inject behavior without modifying core agent logic.
Subagents are specialized AI workers. A code review plugin might include a subagent that handles security scans while the main agent focuses on logic changes. Subagents run in parallel and report results back to the orchestrator, as detailed in our guide to Claude Code subagents.
MCP servers provide protocol-level integrations. They expose tools, resources, and prompts that the agent can invoke through standardized interfaces. An MCP server handles authentication, rate limiting, and API versioning so skills don't duplicate that logic.
%% alt: Plugin architecture showing skills, hooks, subagents, and MCP servers in a hierarchical structure
flowchart TD
Plugin["Plugin Package"]
Skills["Skills Layer"]
Hooks["Lifecycle Hooks"]
Subagents["Subagent Workers"]
MCP["MCP Server"]
Plugin --> Skills
Plugin --> Hooks
Plugin --> Subagents
Plugin --> MCP
Skills --> Task1["Validate Config"]
Skills --> Task2["Deploy Resources"]
Skills --> Task3["Monitor Status"]
Hooks --> Pre["PreToolUse Hook"]
Hooks --> Post["Stop Hook"]
Subagents --> Security["Security Scanner"]
Subagents --> Perf["Performance Profiler"]
MCP --> Auth["Authentication"]
MCP --> API["API Integration"]
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
classDef uiComponent fill:#2a1840,stroke:#c084fc,color:#f3e8ff
classDef dataStore fill:#3a2f0b,stroke:#fbbf24,color:#fef3c7
class Plugin framework
class Skills,Hooks,Subagents,MCP uiComponent
class Task1,Task2,Task3,Pre,Post,Security,Perf,Auth,API dataStore
The distinction between these layers is critical. Skills implement logic. Hooks inject timing. Subagents parallelize work. MCP servers abstract protocols. Mixing these concerns creates brittle plugins that break when the agent runtime evolves.

Creating Your First Plugin: Directory Structure and plugin.json
Plugin development starts with standardized file organization. The runtime expects specific files in specific locations. Deviating from this structure causes installation failures.
The minimal plugin directory looks like this:
my-plugin/
├── .claude-plugin/
│ └── plugin.json // Manifest and metadata
├── skills/ // Task execution
│ ├── validate-config/
│ │ └── SKILL.md
│ └── deploy-resources/
│ └── SKILL.md
├── hooks/ // Lifecycle interception
│ └── hooks.json
├── agents/ // Specialized subagents
│ └── deployment-monitor.md
├── scripts/ // Scripts hooks and skills run
│ └── check-env.sh
├── mcp-server/ // Protocol integration
│ └── aws-connector.js
└── README.md // DocumentationThe plugin.json file carries the plugin's metadata, and only needs to point at a component when that component lives outside its default directory:
{
"name": "aws-deploy-toolkit",
"displayName": "AWS Deploy Toolkit",
"version": "1.2.0",
"description": "Complete AWS deployment automation for Claude Code",
"author": { "name": "your-org" },
"license": "MIT",
"keywords": ["aws", "deployment", "cloudformation"],
"mcpServers": {
"aws-connector": {
"command": "node",
"args": ["${CLAUDE_PLUGIN_ROOT}/mcp-server/aws-connector.js"]
}
}
}The manifest drives installation. When developers run claude plugin install, Claude Code reads plugin.json, loads whatever it declares plus the components sitting in their default directories, and connects the aws-connector MCP server automatically once the plugin is enabled. Running claude plugin validate --strict . before you ship catches a malformed manifest: an unrecognized field, a missing version, or a component path that doesn't exist.
Notice plugin.json doesn't mention skills/, hooks/, or agents/ at all. Claude Code scans skills/, hooks/hooks.json, and agents/ automatically, so validate-config, deploy-resources, the deployment hook, and the deployment-monitor subagent all register themselves just by existing in those folders. The manifest only names mcpServers here because the connector script lives in a custom mcp-server/ folder instead of the default .mcp.json. plugin.json's own dependencies field, if you need it, lists other PLUGINS this one requires, not npm packages: the @aws-sdk/client-cloudformation and zod packages the MCP server imports belong in the mcp-server/ folder's own package.json, not here.
Bundling Multiple Skills Into One Installable Package
Bundling transforms isolated capabilities into a cohesive toolkit. The pattern that works in production involves three steps: identify related operations, share configuration, and expose a unified interface.
Consider a database migration plugin. Three skills handle the workflow: analyze-schema, generate-migration, and apply-changes. Shipping those as separate installs creates version drift—teams run mismatched versions and migrations fail.
The bundled approach consolidates them. Each skill is its own directory under skills/ with its own SKILL.md, and a small bundled script does the actual work:
# skills/analyze-schema/SKILL.md
---
name: analyze-schema
description: Inspects the configured database and reports pending schema changes. Use when migrating a database or checking for schema drift.
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/analyze-schema.sh *)
---
## Steps
1. Run `${CLAUDE_SKILL_DIR}/scripts/analyze-schema.sh` to connect to the configured database and dump its current schema.
2. Compare the dump against the stored baseline and list every table that was added, removed, or altered.
3. Report the changed tables so the next skill can build a migration from them.
# skills/generate-migration/SKILL.md
---
name: generate-migration
description: Builds a migration from the schema changes analyze-schema reported. Use after analyze-schema finds pending changes.
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/generate-migration.sh *)
---
## Steps
1. Using the schema changes reported above, run `${CLAUDE_SKILL_DIR}/scripts/generate-migration.sh` to generate migration SQL for them.
2. Show the generated SQL and the tables it touches before moving on.
# skills/apply-changes/SKILL.md
---
name: apply-changes
description: Applies a generated migration to the configured database. Use only after a human has reviewed the migration SQL.
disable-model-invocation: true
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/apply-changes.sh *)
---
## Steps
1. Confirm the migration SQL from the previous step looks correct.
2. Run `${CLAUDE_SKILL_DIR}/scripts/apply-changes.sh` to apply it to the configured database.
3. Report how many tables were affected.Each skill is independently installable and disableable even though all three ship in one plugin. Claude invokes them in sequence inside the same conversation, so analyze-schema's findings are still in context when generate-migration runs next, and that skill's generated SQL is still in context when apply-changes runs after it. The database connection details live in one config file the bundled scripts read directly, so every skill in the plugin talks to the same database without duplicating connection logic. This matters because it eliminates the fragile state management that breaks multi-step workflows.
%% alt: Workflow showing bundled skills executing in sequence with a shared config file
flowchart TD
Start["User Request: Migrate Database"]
Install["Plugin Install"]
Config["Shared Config File"]
Analyze["Skill: Analyze Schema"]
Generate["Skill: Generate Migration"]
Apply["Skill: Apply Changes"]
Result["Migration Complete"]
Start --> Install
Install --> Config
Config -.->|"Read by"| Analyze
Config -.->|"Read by"| Generate
Config -.->|"Read by"| Apply
Analyze -->|Schema Changes in Context| Generate
Generate -->|Migration SQL in Context| Apply
Apply --> Result
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
classDef dataStore fill:#3a2f0b,stroke:#fbbf24,color:#fef3c7
class Start,Install userAction
class Config,Analyze,Generate,Apply framework
class Result dataStore
The pattern holds because each skill stays independently invokable. Claude can run analyze-schema on its own to check for drift, or run all three in sequence for a full migration, and either way the skills stay isolated: each one is a separate SKILL.md with its own instructions, and nothing but the conversation and the shared config file connects them.
Adding Hooks and MCP Servers to Your Plugin Bundle
Hooks and MCP servers extend plugins beyond task execution. Hooks modify timing. MCP servers abstract external systems. Together, they transform a skill collection into a complete integration layer.
A deployment plugin demonstrates both. The hook validates environment readiness before any deployment command runs. Plugin hooks live in hooks/hooks.json, not a TypeScript module, and they bind a real event to a shell command:
{
"description": "Validates AWS credentials and environment health before any deployment tool call",
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/check-env.sh",
"timeout": 30
}
]
}
]
}
}check-env.sh is a plain shell script bundled with the plugin. Claude Code sends it the PreToolUse event as JSON on stdin before every Bash call; the script checks AWS credentials and the target environment's health, then exits 2 with the reason on stderr to block the command when either check fails. Claude Code reads that stderr as the block reason, the same validation the earlier draft's invented beforeTask throw was reaching for, just through a real hook event.
The MCP server handles AWS API interactions. Skills call the server instead of importing AWS SDKs directly:
// mcp-server/aws-connector.js
import { MCPServer, Tool } from '@modelcontextprotocol/sdk';
export const server = new MCPServer({
name: 'aws-deploy-connector',
version: '1.0.0'
});
server.addTool(new Tool({
name: 'deploy_cloudformation_stack',
description: 'Deploys a CloudFormation stack with automatic retry logic',
inputSchema: {
type: 'object',
properties: {
stackName: { type: 'string' },
templateBody: { type: 'string' },
parameters: { type: 'object' }
},
required: ['stackName', 'templateBody']
},
async execute(input) {
// Centralized deployment logic with error handling
const result = await deployStack({
name: input.stackName,
template: input.templateBody,
params: input.parameters
});
return {
stackId: result.stackId,
status: result.status
};
}
}));Skills invoke the MCP tool instead of calling AWS directly. This separation matters because MCP servers handle cross-cutting concerns—authentication, retries, rate limiting—once. Skills stay focused on business logic.
The hook-first validation prevents wasted API calls. The MCP abstraction prevents vendor lock-in. Together, they make the plugin resilient to infrastructure changes.

Plugin vs Skill vs Extension: When to Package What
The packaging decision determines maintenance burden and adoption friction. Engineers who choose the wrong unit waste months fixing installation issues.
%% alt: Comparison between plugin, skill, and extension packaging approaches
flowchart LR
subgraph PluginApproach["Plugin: Multi-capability bundle"]
P1["Install Once"]
P2["Multiple Skills"]
P3["Shared Config"]
P4["Hooks + MCP"]
P5["Version Lock"]
P1 --> P2
P2 --> P3
P3 --> P4
P4 --> P5
end
subgraph SkillApproach["Skill: Single task"]
S1["Standalone Install"]
S2["One Function"]
S3["No Dependencies"]
S4["Direct Execution"]
S1 --> S2
S2 --> S3
S3 --> S4
end
subgraph ExtensionApproach["Extension: Editor integration"]
E1["UI Modification"]
E2["Editor Events"]
E3["Visual Layer"]
E4["No Agent Logic"]
E1 --> E2
E2 --> E3
E3 --> E4
end
PluginApproach -.->|"Best for: Related workflows"| Decision["Packaging Decision"]
SkillApproach -.->|"Best for: Atomic utilities"| Decision
ExtensionApproach -.->|"Best for: IDE features"| Decision
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
class P1,P2,P3,P4,P5 framework
class S1,S2,S3,S4 userAction
class E1,E2,E3,E4 framework
Plugins suit multi-step workflows. If capabilities depend on each other—schema validation before migration generation—bundle them. The version lock prevents drift. The shared configuration eliminates redundant setup.
Skills suit atomic utilities. A JSON formatter doesn't need hooks or MCP servers. Shipping it as a plugin adds installation weight without value. Standalone skills install faster and compose better.
Extensions modify editor behavior, not agent logic. Syntax highlighting, code folding, and inline suggestions belong in extensions. They react to user actions in the editor, not agent task execution. Mixing extension code into plugins creates runtime confusion because the execution contexts differ.
The failure mode here is subtle but expensive. Teams package everything as plugins "for consistency," then wonder why lightweight utilities take seconds to activate. Or they ship complex workflows as standalone skills, then spend months debugging version conflicts.
The correct heuristic: if it needs coordination, bundle it as a plugin. If it stands alone, ship it as a skill. If it touches the editor UI, build an extension.
Publishing and Distributing Your Plugin to the Marketplace
Publication transforms a local development artifact into a community resource. There's no separate "publish" command. A plugin is published the moment a .claude-plugin/marketplace.json listing it reaches a git repository, and listing it in Anthropic's directory on top of that is a portal submission, not a CLI step.
%% alt: Plugin publication workflow from development to a marketplace listing
flowchart TD
Dev["Local Development"]
Validate["claude plugin validate"]
Marketplace["Add .claude-plugin/marketplace.json"]
Push["Push to Git Repository"]
Published["Published via Marketplace"]
Submit["Submit in Developer Portal"]
Review["Portal Review"]
Directory["Listed in Anthropic's Directory"]
Install["Users Install"]
Dev --> Validate
Validate -->|Pass| Marketplace
Validate -->|Fail| Fix["Fix Issues"]
Fix --> Validate
Marketplace --> Push
Push --> Published
Published --> Install
Push -.->|Optional| Submit
Submit --> Review
Review -->|Approved| Directory
Review -->|Changes requested| Fix
Directory --> Install
classDef userAction fill:#142544,stroke:#7c9cf0,color:#eaf2ff
classDef framework fill:#0b3b2e,stroke:#34d399,color:#d1fae5
classDef dataStore fill:#3a2f0b,stroke:#fbbf24,color:#fef3c7
class Dev,Validate userAction
class Marketplace,Push,Submit,Review framework
class Published,Directory,Install dataStore
class Fix stroke:#ef4444,fill:#450a0a,color:#fca5a5
A build step is only needed if your MCP server ships compiled TypeScript; skills, hooks, and subagent files run straight from the repository with no build at all. Whichever is true for your plugin, put a validate script in package.json so CI runs the same check you run locally:
{
"scripts": {
"build": "tsc",
"validate": "claude plugin validate --strict ."
}
}claude plugin validate --strict . catches common errors: an unrecognized or mistyped field, a component path that doesn't start with ./ or escapes the plugin root, and a misconfigured MCP server entry. Running it before you push prevents a broken plugin from ever reaching users.
Authenticity comes from the repository itself, not a signature. There's no code-signing step and no signed artifact format. Installing from your marketplace means cloning your git repository (or whatever host you point source at), so provenance is ordinary git trust: a private repository limits who can install, and a public one is as trustworthy as the account that owns it. When another plugin declares a version range on yours, tag the release so that range can resolve:
# From the plugin's repository root
claude plugin tag --pushThis creates a {name}--v{version} git tag from the version your plugin.json sets and pushes it to origin.
Marketplace metadata determines discoverability. Set description, author, homepage, and repository in plugin.json, add a README.md at the plugin root, and claude plugin validate reminds you when the README has no line that installs the plugin. Plugins with clear descriptions and a working install line are easier to adopt than minimal listings.
Versioning follows semantic versioning. Increment version in plugin.json on every release, or omit it entirely in a git-hosted marketplace and let Claude Code resolve installs against the commit SHA instead.
Auto-update is off by default for a marketplace, so a user on an older version stays there until they run claude plugin update or you turn auto-update on for them. There's no forced-update mechanism: a critical fix still depends on users pulling the new version, whether by command or by auto-update picking it up on a delay.
Real-World Plugin Examples and Best Practices
Production plugins reveal patterns that academic examples miss. The stripe-integration plugin demonstrates complete workflow bundling. It packages payment processing skills, webhook handling hooks, a subscription management subagent, and an MCP server for Stripe API access.
The architecture avoids common mistakes. Skills don't call Stripe APIs directly—they invoke MCP tools. A PreToolUse hook validates API keys before any payment operation. The subagent monitors subscription renewals in parallel with the main agent's work. This separation of concerns makes the plugin testable and maintainable.
The code-review-suite plugin shows the value of subagent parallelization. Security scanning, performance profiling, and style checking run concurrently instead of sequentially, as covered in our analysis of 2-million-token context windows. The main agent orchestrates results without waiting for each check to complete.
Best practices from these examples:
- Keep each skill's
SKILL.mdunder 500 lines. Move detailed reference material or examples into separate files the skill links to, and keep heavier logic in a bundled script instead of the instructions themselves. - If a skill bundles a script, write it in whatever language fits the job (Bash, Python, Node) and reference it with
${CLAUDE_SKILL_DIR}inallowed-toolsso the exact command runs without a permission prompt. - Version lock all dependencies in
plugin.json. Loose version ranges cause installation failures when breaking changes ship. - Validate locally before you push:
claude plugin validate --strict ./your-plugincatches manifest errors, andclaude --plugin-dir ./your-pluginloads the plugin for a real session so you can exercise it end to end instead of trusting unit tests alone. - Document configuration options in README with working examples. Most support requests stem from misconfigured plugins.
The failure patterns repeat across failed plugins. Tight coupling between skills breaks when runtimes upgrade. Missing error boundaries cause one skill failure to crash the entire plugin. Undocumented dependencies create installation loops where users can't resolve missing packages.
Engineers who avoid these patterns ship plugins that install cleanly and run reliably. The maintenance burden drops because the architecture anticipates change. Teams building autonomous PR workflows rely on this plugin stability—the agent can't review code if the review plugin crashes on import.
That covers the essential patterns for Claude Code plugin packaging. Bundle related capabilities together, separate concerns across layers, and validate before publishing. Apply these in production and the difference will be immediate: installations succeed, workflows compose cleanly, and maintenance costs drop. The plugin architecture matters because it's the foundation that determines whether your agent toolkit scales or collapses under its own complexity.