How the Claude Plugin Manifest Is Parsed and Validated in compound-engineering-plugin

The Claude plugin manifest is parsed by loadClaudePlugin in src/parsers/claude.ts, which validates the JSON structure, resolves component directories, and enforces path containment to prevent directory traversal attacks before returning a fully typed ClaudePlugin object.

The compound-engineering-plugin repository by EveryInc provides a robust parser for Claude Code plugins. Understanding how the Claude plugin manifest is parsed and validated ensures developers can securely load custom agents, commands, and MCP servers while maintaining strict path containment.

Locating and Reading the Manifest File

Resolving the Plugin Root

The entry point loadClaudePlugin calls resolveClaudeRoot (lines 39-55 in src/parsers/claude.ts) to locate the manifest. This function accepts either a direct path to .claude-plugin/plugin.json or a directory containing it. If the file cannot be found, the function throws an error immediately.

Parsing the JSON Manifest

Once located, the manifest is read using readJson<ClaudeManifest> and stored as a typed object. This occurs at line 19 in src/parsers/claude.ts, where the raw JSON is deserialized into the ClaudeManifest interface defined in src/types/claude.ts.

Validating Component Paths and Security

Preventing Directory Traversal Attacks

Security is enforced by resolveWithinRoot (lines 45-52 in src/parsers/claude.ts). This function checks that any resolved path is either exactly the plugin root or a subdirectory using resolvedPath.startsWith(resolvedRoot + path.sep). If a custom path attempts to escape the root (e.g., ../outside), the parser throws a clear error message preventing the directory traversal attack.

Resolving Custom Component Directories

The manifest allows custom paths for agents, commands, skills, and hooks via resolveComponentDirs (lines 81-90 in src/parsers/claude.ts). This function builds an array starting with default directories (agents/, commands/, etc.) and appends validated custom paths. Each custom path undergoes the resolveWithinRoot check before inclusion.

Loading Plugin Components

Agents, Commands, and Skills

Component loading occurs in loadAgents, loadCommands, and loadSkills (lines 57-99 in src/parsers/claude.ts). These functions use collectMarkdownFiles to gather markdown files from the resolved directories, then parse each file's frontmatter using parseFrontmatter from src/utils/frontmatter.ts. The frontmatter fields (e.g., name, description, allowed-tools) are extracted and typed according to src/types/claude.ts.

Hooks Configuration

Hooks are loaded via loadHooks (lines 21-44 in src/parsers/claude.ts). The parser first checks for a default hooks/hooks.json file, then processes any custom hook paths defined in the manifest. After loading multiple hook configurations, mergeHooks combines them into a single ClaudeHooks map, with later configurations overriding earlier ones.

MCP Server Definitions

MCP servers are handled by loadMcpServers (lines 46-64 in src/parsers/claude.ts). Definitions may be inline in the manifest, referenced via file paths, or stored in a .mcp.json file at the plugin root. All path references undergo the same resolveWithinRoot validation before being loaded and merged into a unified map of server configurations.

Complete Plugin Loading Example

import { loadClaudePlugin } from "./src/parsers/claude"

const pluginRoot = "/path/to/plugins/compound-engineering"

try {
  const plugin = await loadClaudePlugin(pluginRoot)
  console.log(`✅ Loaded ${plugin.manifest.name} v${plugin.manifest.version}`)
  console.log(`Agents:   ${plugin.agents.length}`)
  console.log(`Commands: ${plugin.commands.length}`)
  console.log(`MCP Servers: ${Object.keys(plugin.mcpServers).length}`)
} catch (err) {
  console.error("Failed to load plugin:", err.message)
}

Summary

  • The parser enforces strict path containment via resolveWithinRoot to prevent directory traversal attacks.
  • Custom component directories are supported but must validate against the plugin root before loading.
  • Frontmatter parsing extracts metadata from markdown files for agents, commands, and skills using parseFrontmatter.
  • Hooks and MCP servers support configuration merging from multiple sources (inline, default files, and custom paths).
  • The entire pipeline returns a fully typed ClaudePlugin object defined in src/types/claude.ts.

Frequently Asked Questions

What happens if the plugin.json file is missing?

If the manifest file cannot be found at .claude-plugin/plugin.json or the specified path, resolveClaudeRoot throws an error immediately. This prevents the parser from attempting to load an invalid or incomplete plugin structure.

How does the parser prevent path traversal attacks?

The resolveWithinRoot function (lines 45-52 in src/parsers/claude.ts) validates that all resolved paths start with the plugin root directory using resolvedPath.startsWith(resolvedRoot + path.sep). Any path attempting to escape the root (e.g., containing ../) triggers a clear error message.

Can I use custom directories for agents and commands?

Yes. The manifest supports custom paths for agents, commands, skills, and hooks. The resolveComponentDirs function processes these paths, but each must pass the resolveWithinRoot validation to ensure it remains within the plugin directory.

What is the difference between hooks and MCP servers?

Hooks are event-driven configurations loaded from hooks.json files that define callbacks or triggers within the Claude plugin lifecycle. MCP (Model Context Protocol) servers are external service definitions that may be inline, referenced by path, or stored in .mcp.json, enabling the plugin to connect to external tools and data sources.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →