Hmbown/CodeWhale · error
source is not a valid plugin bundle
Error message
source is not a valid plugin bundle: {error} What it means
The local source had a manifest, but `PluginManifest::validate_from_path` failed on it (malformed JSON/TOML, missing required fields, invalid types). The validator's message is embedded as `{error}`; the source bundle is rejected before staging.
Solutions
- Fix the specific issue named in the embedded `{error}` detail.
- Run `jq . plugin.json` (or a TOML linter) to catch syntax errors.
- Compare against the current plugin manifest schema and add missing required fields.
- Pull the latest plugin source — an outdated checkout may carry an outdated manifest.
Example fix
// before: plugin.json (missing version, trailing comma)
{ "plugin": { "name": "my-plugin", } }
// after
{ "plugin": { "name": "my-plugin", "version": "1.0.0" } } Defensive patterns
Strategy: validation
Validate before calling
try {
const raw = fs.readFileSync(path.join(src, 'plugin.json'), 'utf8');
const m = JSON.parse(raw);
if (!m.plugin?.name || !m.plugin?.version) throw new Error('missing plugin.name/version');
} catch (e) {
throw new Error(`invalid plugin manifest in ${src}: ${e.message}`);
} Type guard
function manifestParses(p) {
try { const m = JSON.parse(fs.readFileSync(path.join(p, 'plugin.json'), 'utf8')); return !!m?.plugin?.name; }
catch { return false; }
} Try / catch
try {
await codewhale.plugins.install(srcDir);
} catch (e) {
if (e.message.includes('source is not a valid plugin bundle:')) {
console.error('manifest invalid — see detail:', e.message);
} else throw e;
} Prevention
- Validate the manifest locally (jq / schema check) before each install.
- Avoid trailing commas and type mismatches when hand-editing plugin.json.
- Keep the manifest schema up to date with the codewhale version you run.
When it happens
Trigger: `stage_local_copy` (called by `install_inner`) calls `PluginManifest::validate_from_path(&manifest_path)` after locating the manifest and the validation returns an error.
Common situations: Editing plugin.json by hand and introducing a syntax error or missing required field, manifests from an older schema version, trailing commas in JSON, wrong value types.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- staged plugin manifest failed validation
- installed plugin has no supported manifest
- source is not a valid plugin bundle: no plugin.json…
- staged bundle has no plugin.json…
- plugin name is not a safe directory name
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/0a07f26a948abd8b.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/plugins/install/stage.rs:100
}
/// 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()
);
}
let staged_path = fresh_staging_dir(user_plugins_dir)?;
let result = (|| -> Result<StagedPlugin> {
let mut budget = CopyBudget::default();
copy_bundle_regular_files(&canonical_source, &staged_path, max_size, &mut budget)?;View on GitHub (pinned to 73e0f67d83)