Hmbown/CodeWhale · error
installed plugin has no supported manifest
Error message
installed plugin has no supported manifest
What it means
After finalizing a plugin install by moving files into place, the installed tree must contain a recognizable plugin manifest (plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml). `resolve_manifest_path` returned None, so the installed directory is not a supported plugin. The install is aborted and rolled back (restoring any backup).
Solutions
- Verify the bundle contains a supported manifest at its root or under .claude-plugin/ (plugin.json, kimi.plugin.json, or plugin.toml).
- Re-download or re-export the plugin archive; the packaging step likely dropped the manifest.
- If publishing a plugin, add the manifest at the expected location and reinstall.
- Check whether the archive unpacked into an extra nested directory and flatten it.
Example fix
// before (zip layout: myplugin/dist/plugin.json) zip -r myplugin.zip myplugin // after (manifest at bundle root) zip -r myplugin.zip myplugin # with myplugin/plugin.json at top level
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
const SUPPORTED = ['plugin.json', '.claude-plugin/plugin.json', 'kimi.plugin.json', 'plugin.toml'];
function bundleHasManifest(dir) {
return SUPPORTED.some((p) => fs.existsSync(`${dir}/${p}`));
}
if (!bundleHasManifest('dist/myplugin')) throw new Error('no manifest in bundle'); Type guard
function isPluginBundleDir(p) {
return p != null && typeof p === 'string' &&
['plugin.json', 'kimi.plugin.json', 'plugin.toml'].some((f) =>
fs.existsSync(path.join(p, f)) || fs.existsSync(path.join(p, '.claude-plugin', f)));
} Try / catch
try {
await codewhale.plugins.install(archivePath);
} catch (e) {
if (e.message.includes('no supported manifest')) {
console.error('bundle missing plugin.json — repack with manifest at root');
} else throw e;
} Prevention
- Keep the manifest at the bundle root, never nested under a wrapper directory.
- Verify archive contents with `tar tzf`/`unzip -l` before installing.
- Add a packaging test asserting the manifest ships in the artifact.
When it happens
Trigger: `finalize_install` (called from `install_inner` and `install_remote_bytes`) calls `resolve_manifest_path(&final_path)` on the just-installed tree and it finds no supported manifest file.
Common situations: Installing a tarball or repo whose manifest is nested deeper than expected, a manifest that was deleted or renamed during packaging, downloading a non-plugin release archive, or a manifest format not in the supported set.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- source is not a valid plugin bundle: no plugin.json…
- staged bundle has no plugin.json…
- source is not a valid plugin bundle
- staged plugin manifest failed validation
- bundle exceeds the file limit
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/b1fbdb58b37deb48.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/plugins/install/place.rs:91
// Write the marker last so a partial install never leaves a stale
// `.installed-from` on disk.
if let Err(error) = skill_install::write_installed_from_v2(
&final_path,
spec,
url,
source_checksum,
&staged.content_hash,
&staged.name,
) {
let _ = fs::remove_dir_all(&final_path);
if let Some(backup) = backup_path.take() {
let _ = fs::rename(&backup, &final_path);
}
return Err(error);
}
let installed_content_hash =
match crate::plugins::agent_plugin::resolve_manifest_path(&final_path)
.ok_or_else(|| anyhow::anyhow!("installed plugin has no supported manifest"))
.and_then(|manifest_path| {
crate::plugins::manifest::PluginManifest::validate_from_path(&manifest_path)
.map(|validated| validated.content_hash)
.map_err(anyhow::Error::msg)
}) {
Ok(hash) => hash,
Err(error) => {
let _ = fs::remove_dir_all(&final_path);
if let Some(backup) = backup_path.take() {
let _ = fs::rename(&backup, &final_path);
}
return Err(error).context("installed plugin failed post-copy validation");
}
};
if let Some(backup) = backup_path {
fs::remove_dir_all(&backup).ok();
}
View on GitHub (pinned to 73e0f67d83)