Hmbown/CodeWhale · error

source is not a valid plugin bundle: no plugin.json…

Error message

source is not a valid plugin bundle: no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml

What it means

When installing from a local directory, the source is validated before copying. `resolve_manifest_path(source)` finds no supported manifest (plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml), so the directory is not a plugin bundle and staging is refused. This check also rejects symlinked roots and manifests.

Solutions

  1. cd into the directory and confirm plugin.json (or an equivalent) exists at its root.
  2. Point install at the inner plugin directory rather than its parent or repo root.
  3. Rename your manifest to a supported filename (plugin.json, kimi.plugin.json, or plugin.toml) or place it in .claude-plugin/.
  4. If installing from a repo, check out the tag/branch where the manifest exists.

Example fix

// before
codewhale plugin install ./myrepo          # no manifest at repo root
// after
codewhale plugin install ./myrepo/plugins/my-plugin  # contains plugin.json
Defensive patterns

Strategy: validation

Validate before calling

const SUPPORTED = ['plugin.json', '.claude-plugin/plugin.json', 'kimi.plugin.json', 'plugin.toml'];
const src = process.argv[2];
if (!SUPPORTED.some((f) => fs.existsSync(path.join(src, f)))) {
  console.error(`${src} is not a plugin bundle (no manifest)`); process.exit(1);
}

Type guard

const isPluginSource = (p) => fs.existsSync(path.join(p, 'plugin.json')) ||
  fs.existsSync(path.join(p, '.claude-plugin', 'plugin.json')) ||
  fs.existsSync(path.join(p, 'kimi.plugin.json')) ||
  fs.existsSync(path.join(p, 'plugin.toml'));

Try / catch

try {
  await codewhale.plugins.install(srcDir);
} catch (e) {
  if (e.message.includes('source is not a valid plugin bundle')) {
    console.error(`check that ${srcDir} contains a supported manifest`);
  } else throw e;
}

Prevention

When it happens

Trigger: `stage_local_copy` (called by `install_inner`) is given a local path where no supported manifest file exists at the root or under .claude-plugin/.

Common situations: `codewhale plugin install ./path` pointed at the wrong directory (parent, repo root, or a non-plugin folder), a plugin using an unsupported manifest name, or the manifest accidentally deleted/ignored by packaging.

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/e39ebd9eeb139d4b. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/plugins/install/stage.rs:95

    let name = validated.manifest.plugin.name.clone();
    validate_skill_name_segment(&name).map_err(|error| {
        anyhow::anyhow!("[plugin].name is not a safe directory name: {error:#}")
    })?;
    Ok((name, validated.content_hash))
}

/// Copy a local bundle directory into staging. Symlinks anywhere in the
/// source are rejected; a stale `.installed-from` marker is never copied so
/// provenance always reflects *this* install.
pub(super) fn stage_local_copy(
    source: &Path,
    user_plugins_dir: &Path,
    max_size: u64,
) -> Result<StagedPlugin> {
    // Validate the source first; this also rejects symlinked roots/manifests.
    let manifest_path =
        crate::plugins::agent_plugin::resolve_manifest_path(source).ok_or_else(|| {
            anyhow::anyhow!(
                "source is not a valid plugin bundle: no plugin.json, .claude-plugin/plugin.json, kimi.plugin.json, or plugin.toml"
            )
        })?;
    PluginManifest::validate_from_path(&manifest_path)
        .map_err(|error| anyhow::anyhow!("source is not a valid plugin bundle: {error}"))?;
    let canonical_source = source
        .canonicalize()
        .with_context(|| format!("failed to resolve {}", source.display()))?;
    if let Ok(canonical_plugins) = user_plugins_dir.canonicalize()
        && (canonical_source == canonical_plugins
            || canonical_source.starts_with(&canonical_plugins))
    {
        bail!(
            "cannot install a bundle from inside the user plugins directory {}; \
             it is already in place",
            canonical_plugins.display()
        );
    }

View on GitHub (pinned to 73e0f67d83)