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,keywordsfor metadataagents,commands,skillsfor component pathshooksfor lifecycle hooksmcpServersfor 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
nameandversionas strings, defined insrc/types/claude.ts. - Path security: All custom paths for components, hooks, and MCP servers must resolve within the plugin root via
resolveWithinRoot()insrc/parsers/claude.ts. - Flexible configuration: Optional fields support multiple formats (strings, arrays, or objects) with normalization via
toPathList(). - MCP fallback: If
mcpServersis omitted, the loader falls back to.mcp.jsonin 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →