Skip to content
jsmanifest logojsmanifest

Packaging Claude Code Plugins: Bundle Skills, Hooks, Subagents, and MCP Servers

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.json at the plugin's root and described skills, hooks, and subagents as TypeScript modules with an "entry" file, a SkillContext/HookContext API, a "capabilities" manifest wrapper, and a "runtime"/npm-style "dependencies" block. None of that exists. The manifest lives at .claude-plugin/plugin.json with fields such as name, version, author (an object), and mcpServers; a skill is a directory with a SKILL.md file; a hook is defined in hooks/hooks.json against real events like PreToolUse and Stop, running a shell command that reads JSON on stdin; and a subagent is a Markdown file under agents/ with YAML frontmatter. The post's publishing steps (code signing, a claude plugin publish command) were also invented; publishing means adding a .claude-plugin/marketplace.json file 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
Plugin architecture showing skills, hooks, subagents, and MCP servers in a hierarchical structure

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.

Plugin architecture diagram showing the relationship between components

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            // Documentation

The 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
Workflow showing bundled skills executing in sequence with a shared config file

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.

MCP server integration showing protocol abstraction layer

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
Comparison between plugin, skill, and extension packaging approaches

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
Plugin publication workflow from development to a marketplace listing

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 --push

This 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.md under 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} in allowed-tools so 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-plugin catches manifest errors, and claude --plugin-dir ./your-plugin loads 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.