bmad-code-org/BMAD-METHOD · error · Error

Cannot install module '${resolved.code}': skill directories

Error message

Cannot install module '${resolved.code}': skill directories '${seenLeaves.get(skillDirName)}' and '${skillPath}' share the leaf name '${skillDirName}' and would overwrite each other. Skill directory names must be unique.

What it means

Thrown by OfficialModules._copyResolvedSkills when a resolved module's skillPaths contain two directories whose path.basename() (leaf) collide. Skills are flattened into the target directory by leaf name, so a collision would silently overwrite one skill with the other; the installer throws to fail loud. Both marketplace-plugin installs (install) and custom-source installs (installFromResolution) route through this method, so the guard covers both.

Source

Thrown at tools/installer/modules/official-modules.js:349

   * at the module root. Shared by both custom marketplace installs
   * (installFromResolution) and official marketplace-plugin registry installs
   * (install), so the two paths cannot drift.
   * @param {Object} resolved - ResolvedModule from PluginResolver
   * @param {string} targetPath - Destination module directory (e.g. bmadDir/<code>)
   * @param {Function} fileTrackingCallback - Optional callback to track installed files
   * @param {Object} moduleConfig - Module configuration passed to copy filtering
   */
  async _copyResolvedSkills(resolved, targetPath, fileTrackingCallback = null, moduleConfig = {}) {
    await fs.ensureDir(targetPath);

    // Copy each skill directory, flattened by leaf name. Leaf names must be
    // unique — two skills that flatten to the same directory would silently
    // overwrite each other, so fail loud instead.
    const seenLeaves = new Map();
    for (const skillPath of resolved.skillPaths) {
      const skillDirName = path.basename(skillPath);
      if (seenLeaves.has(skillDirName)) {
        throw new Error(
          `Cannot install module '${resolved.code}': skill directories '${seenLeaves.get(skillDirName)}' and ` +
            `'${skillPath}' share the leaf name '${skillDirName}' and would overwrite each other. ` +
            `Skill directory names must be unique.`,
        );
      }
      seenLeaves.set(skillDirName, skillPath);
      const skillTarget = path.join(targetPath, skillDirName);
      await this.copyModuleWithFiltering(skillPath, skillTarget, fileTrackingCallback, moduleConfig);
    }

    // Place module-help.csv at the module root.
    const helpTarget = path.join(targetPath, 'module-help.csv');
    if (resolved.moduleHelpCsvPath) {
      // Strategies 1-4: copy the existing file.
      await fs.copy(resolved.moduleHelpCsvPath, helpTarget, { overwrite: true });
      if (fileTrackingCallback) fileTrackingCallback(helpTarget);
    } else if (resolved.synthesizedHelpCsv) {
      // Strategy 5: write synthesized content.

View on GitHub (pinned to b70486b9bd)

Solutions

  1. Inspect the two paths printed in the error: they share the trailing directory name. Rename one of the offending skill directories in the module source so every leaf is unique.
  2. If you don't control the source (remote marketplace/custom URL), report the duplicate-leaf packaging bug to the module maintainer and pin a known-good version via --pin or a prior commit.
  3. Re-run install/update after the rename. Verify with: ls the resolved skillPaths and confirm no two share a basename.

Example fix

// before (module.yaml declares two skills with same leaf)
//   skills:
//     - path: vendor/foo/skills/research
//     - path: vendor/bar/skills/research   // collides on 'research'
//
// after: rename one directory in the source repo
//   skills:
//     - path: vendor/foo/skills/research
//     - path: vendor/bar/skills/discovery-research
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueSkillLeaves(resolved) {
  const seen = new Map();
  for (const p of resolved.skillPaths || []) {
    const leaf = path.basename(p);
    if (seen.has(leaf)) {
      throw new Error(`Duplicate skill leaf '${leaf}': ${seen.get(leaf)} and ${p}`);
    }
    seen.set(leaf, p);
  }
}
// call before installFromResolution / install
assertUniqueSkillLeaves(resolved);
await modules.installFromResolution(resolved, bmadDir);

Type guard

function isResolvedModule(r) {
  return r && typeof r === 'object'
    && typeof r.code === 'string'
    && Array.isArray(r.skillPaths)
    && r.skillPaths.every((p) => typeof p === 'string');
}

Try / catch

try {
  await modules.installFromResolution(resolved, bmadDir, tracker, opts);
} catch (e) {
  if (/share the leaf name/.test(e.message)) {
    // module packaging bug — surface the pair and abort this module
    console.error('Module has duplicate skill leaf names; fix the source:', e.message);
    continue;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling installer.install() or installer.installFromResolution() for a module whose module.yaml/manifest declares two skill paths that end in the same directory name, e.g. ['vendor/foo/skills/bar', 'vendor/baz/skills/bar']. The basename 'bar' is seen twice and the second triggers the throw.

Common situations: A module author reorganizes skill folders but leaves two directories with identical leaf names. A marketplace plugin aggregates skills from multiple vendors without namespacing. Two modules were merged into one without renaming nested skill dirs. Custom --custom-source repo structure changed and now produces duplicate leaves.

Related errors


AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13). Data as JSON: /api/errors/4041ed1b50887b7a. Report an issue: GitHub.