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
- 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.
- 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.
- 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 authoring a module, namespace nested skill dirs (vendor/skill) and run a pre-publish check that all skillPath leaves are unique.
- Add a unit test that asserts every shipped module's skillPaths have distinct basenames.
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
- ${label} does not exist: ${dirPath}
- ${label} is not a directory: ${dirPath}
- ${label} is not readable: ${dirPath}
- ${label} does not exist: ${filePath}
- ${label} is not a file: ${filePath}
AI-assisted analysis of bmad-code-org/BMAD-METHOD@b70486b9bd (2026-08-13).
Data as JSON: /api/errors/4041ed1b50887b7a.
Report an issue: GitHub.