Validation Rules for Plugin Manifests in the Compound Engineering Plugin

The Compound Engineering Plugin enforces strict validation rules on plugin.json manifests during loading, requiring valid JSON syntax, mandatory name and version string fields, and path entries that must resolve within the plugin root directory to prevent directory traversal attacks.

When loading a Claude plugin, the loadClaudePlugin() function in the EveryInc/compound-engineering-plugin repository performs comprehensive validation on the manifest file located at .claude-plugin/plugin.json. These validation rules for plugin manifests ensure security, structural integrity, and compatibility without relying on external schema validation libraries.

Required and Optional Manifest Fields

The ClaudeManifest type defined in src/types/claude.ts establishes the contract for valid manifests.

Mandatory Fields

Every manifest must include two required top-level fields:

  • name: A string identifying the plugin
  • version: A string specifying the semantic version

These fields are non-optional in the TypeScript interface (lines 10-13), and downstream code assumes their presence.

Optional Fields

The manifest may optionally include:

  • description, author, keywords for metadata
  • agents, commands, skills for component paths
  • hooks for lifecycle hooks
  • mcpServers for Model Context Protocol server configurations

Path Validation and Security Rules

The most critical validation rules for plugin manifests involve path resolution security. The system prevents directory traversal attacks through strict path validation.

The resolveWithinRoot Guard

In src/parsers/claude.ts (lines 45-52), the resolveWithinRoot() function enforces that all custom paths must remain within the plugin root directory:

function resolveWithinRoot(root: string, entry: string, label: string): string {
  const resolvedRoot = path.resolve(root)
  const resolvedPath = path.resolve(root, entry)
  if (resolvedPath === resolvedRoot || resolvedPath.startsWith(resolvedRoot + path.sep)) {
    return resolvedPath
  }
  throw new Error(`Invalid ${label}: ${entry}. Paths must stay within the plugin root.`)
}

Component Path Validation

For agents, commands, and skills fields, the resolveComponentDirs() function (lines 81-90) normalizes entries using toPathList() and validates each through resolveWithinRoot(). Entries must be either strings or arrays of strings representing paths relative to the plugin root.

Hooks and MCP Server Path Validation

The loadHooks() function (lines 21-44) and loadMcpServers() function (lines 151-164) apply the same path validation. For hooks, values can be strings, string arrays, or full ClaudeHooks objects. For mcpServers, values can be objects, strings, or string arrays, with paths resolved through loadMcpPaths()resolveWithinRoot().

MCP Server Configuration Fallback

When the manifest omits the mcpServers field, the loader implements a fallback mechanism. In src/parsers/claude.ts (lines 59-64), loadMcpServers() checks for the presence of a .mcp.json file in the plugin root directory and reads it if the manifest does not provide explicit MCP configuration.

JSON Syntax and Error Handling

Valid JSON Requirement

The readJson() utility (referenced in src/parsers/claude.ts line 19 and implemented in src/utils/files.ts) parses the manifest file and throws immediately if the JSON is malformed.

Explicit Error Messages

Validation failures produce specific error messages that identify the problematic field and the offending path. The test suite in tests/claude-parser.test.ts (lines 94-108) verifies these messages for invalid component paths, hook paths, and MCP paths, ensuring developers receive actionable feedback when validation rules for plugin manifests are violated.

Code Examples

Loading a Well-Formed Plugin

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

const pluginRoot = path.join(import.meta.dir, "fixtures", "sample-plugin")
const plugin = await loadClaudePlugin(pluginRoot)

console.log(plugin.manifest.name)        // "compound-engineering"
console.log(plugin.manifest.version)     // "1.0.0"

The manifest must contain at least name and version; the rest is optional.

Invalid Custom Path – Throws

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

await loadClaudePlugin("fixtures/invalid-command-path")
  // → Error: Invalid commands path: ../outside-commands. Paths must stay within the plugin root.

The same check applies to hooks and mcpServers paths.

Falling Back to .mcp.json

const plugin = await loadClaudePlugin("fixtures/mcp-file")
console.log(plugin.mcpServers?.remote?.url) // "https://example.com/stream"

When the manifest does not define mcpServers, the loader reads .mcp.json.

Summary

  • Mandatory fields: Every manifest must include name and version as strings, defined in src/types/claude.ts.
  • Path security: All custom paths for components, hooks, and MCP servers must resolve within the plugin root via resolveWithinRoot() in src/parsers/claude.ts.
  • Flexible configuration: Optional fields support multiple formats (strings, arrays, or objects) with normalization via toPathList().
  • MCP fallback: If mcpServers is omitted, the loader falls back to .mcp.json in the plugin root.
  • Explicit errors: Validation failures throw descriptive messages identifying the specific field and path violation.

Frequently Asked Questions

What happens if the plugin.json file contains invalid JSON?

The readJson() function throws an error immediately during the loading process, preventing the plugin from initializing. This check occurs in src/parsers/claude.ts before any field validation begins.

Can I use absolute paths for agents, commands, or skills in the manifest?

No. The resolveWithinRoot() function explicitly rejects any path that resolves outside the plugin root directory. All paths must be relative to the plugin root and remain within its boundaries to prevent directory traversal attacks.

What is the difference between hooks and skills in the manifest validation?

While both support string or array-of-string path specifications, skills are validated through resolveComponentDirs() alongside agents and commands, whereas hooks are processed by loadHooks() which additionally accepts a full ClaudeHooks object format. Both undergo the same resolveWithinRoot() security check.

Is the mcpServers field required in every plugin manifest?

No. The mcpServers field is optional. When omitted, the loader automatically checks for a .mcp.json file in the plugin root directory and uses that as the MCP configuration source instead.

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 →