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

  1. Verify the bundle contains a supported manifest at its root or under .claude-plugin/ (plugin.json, kimi.plugin.json, or plugin.toml).
  2. Re-download or re-export the plugin archive; the packaging step likely dropped the manifest.
  3. If publishing a plugin, add the manifest at the expected location and reinstall.
  4. 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

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


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)