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
- cd into the directory and confirm plugin.json (or an equivalent) exists at its root.
- Point install at the inner plugin directory rather than its parent or repo root.
- Rename your manifest to a supported filename (plugin.json, kimi.plugin.json, or plugin.toml) or place it in .claude-plugin/.
- 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
- Run install from inside the plugin directory, not the repo root.
- Symlink only whole plugin directories; the check rejects symlinked roots/manifests.
- Confirm the checkout/branch actually contains the manifest.
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
- installed plugin has no supported manifest
- 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/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)