EveryInc/compound-engineering-plugin · error · Error

Invalid ${label}: ${entry}. Paths must stay within the plugi

Error message

Invalid ${label}: ${entry}. Paths must stay within the plugin root.

What it means

`resolveWithinRoot()` resolves a declared path entry against the plugin root and enforces that the result stays inside that root. If the entry escapes (via `..`, absolute paths, or symlinks resolving outside), the parser throws this error to block path traversal from plugin manifests.

Source

Thrown at src/parsers/claude.ts:269

function unwrapMcpServers(raw: Record<string, unknown>): Record<string, ClaudeMcpServer> {
  if (raw.mcpServers && typeof raw.mcpServers === "object") {
    return raw.mcpServers as Record<string, ClaudeMcpServer>
  }
  return raw as Record<string, ClaudeMcpServer>
}

function mergeMcpConfigs(configs: Record<string, ClaudeMcpServer>[]): Record<string, ClaudeMcpServer> {
  return configs.reduce((acc, config) => ({ ...acc, ...config }), {})
}

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.`)
}

View on GitHub (pinned to c9c10f8c75)

Solutions

  1. Change the manifest entry to a path relative to the plugin root (e.g. `skills/ce-plan` instead of `../ce-plan`)
  2. Copy shared files into the plugin rather than referencing them via traversal
  3. Remove any absolute paths from plugin.json and re-declare them as relative paths
  4. If you own the plugin, restructure so all referenced assets live under the plugin root

Example fix

// before (plugin.json)
{ "skills": { "source": "../shared-skills" } }
// after
{ "skills": { "source": "skills" } }
Defensive patterns

Strategy: validation

Validate before calling

import path from "node:path"
function staysWithinRoot(root: string, entry: string): boolean {
  const r = path.resolve(root)
  const p = path.resolve(root, entry)
  return p === r || p.startsWith(r + path.sep)
}
if (!staysWithinRoot(pluginRoot, declaredPath)) throw new Error(`Refusing: ${declaredPath} escapes the plugin root`)

Type guard

function isWithinRoot(root: string, entry: string): boolean {
  const resolved = path.resolve(root, entry)
  return resolved === path.resolve(root) || resolved.startsWith(path.resolve(root) + path.sep)
}

Try / catch

try {
  const plugin = await loadClaudePlugin(pluginRoot)
} catch (error) {
  if (String(error).includes("Paths must stay within the plugin root")) {
    console.error("Fix the escaping path in the plugin manifest; copy shared files into the plugin instead")
  } else throw error
}

Prevention

When it happens

Trigger: Calling the Claude parser on a plugin whose `.claude-plugin/plugin.json` (or component dir config) declares entries like `"../shared/skills"`, an absolute path such as `/etc/…`, or `"../../outside"` for skills/agents/commands directories.

Common situations: Authoring a plugin that tries to share files across skill/plugin boundaries with `..` traversal (explicitly banned by this repo's conventions); converting a third-party plugin with absolute paths from another machine; moving a plugin checkout without fixing absolute paths in its manifest.

Related errors


AI-assisted analysis of EveryInc/compound-engineering-plugin@c9c10f8c75 (2026-08-31). Data as JSON: /api/errors/870d9764007b1e6e. Report an issue: GitHub.