Hmbown/CodeWhale · error · TypeError
pipeline(): expected an array of items
Error message
pipeline(): expected an array of items
What it means
plugin_target_path computes the destination directory name from the plugin's name and runs it through validate_skill_name_segment (skills/install.rs:1536). A name is rejected if it is empty, has leading/trailing or any internal whitespace, equals '.' or '..', contains '/' or '\\', or is not a single normal path component. This is a path-traversal guard: the name becomes a directory under the user plugins dir, so it must be exactly one safe segment.
Source
Thrown at crates/workflow-js/src/vm.rs:1095
}
return Promise.all(thunks.map((thunk) => {
try {
return Promise.resolve(typeof thunk === "function" ? thunk() : thunk).catch((err) => {
if (isFatalTaskError(err)) throw err;
hostLog("parallel(): dropped a failed slot as null: " + String((err && err.message) || err));
return null;
});
} catch (err) {
if (isFatalTaskError(err)) return Promise.reject(err);
hostLog("parallel(): dropped a failed slot as null: " + String((err && err.message) || err));
return null;
}
}));
};
globalThis.pipeline = (items, ...stages) => {
if (!Array.isArray(items)) {
throw new TypeError("pipeline(): expected an array of items");
}
if (items.length > MAX_ITEMS) {
throw new Error("pipeline(): max " + MAX_ITEMS + " items per call");
}
return Promise.all(items.map(async (item, index) => {
let value = item;
for (const stage of stages) {
try {
value = await stage(value, item, index);
} catch (err) {
if (isFatalTaskError(err)) throw err;
hostLog("pipeline(): dropped item " + index + " as null: " + String((err && err.message) || err));
return null;
}
}
return value;
}));
};View on GitHub (pinned to 8880682c63)
Solutions
- Change the manifest name to a slug: lowercase letters, digits, and hyphens only, no spaces or path separators (e.g. "my-plugin").
- Check for invisible characters: leading/trailing spaces, non-breaking spaces, or a BOM before the name in the JSON.
- Keep a separate display/description field for the pretty name; the name field is a directory identifier.
Example fix
// before (plugin.json)
{ "plugin": { "name": "My Fancy Plugin", ... } }
// after
{ "plugin": { "name": "my-fancy-plugin", "description": "My Fancy Plugin" } } Defensive patterns
Strategy: validation
Validate before calling
let name = &manifest.plugin.name;
let safe = !name.is_empty()
&& name.trim() == name
&& !name.chars().any(char::is_whitespace)
&& !name.contains('/') && !name.contains('\\')
&& name != "." && name != "..";
if !safe {
anyhow::bail!("choose a slug-style plugin name (letters, digits, hyphens)");
} Type guard
fn is_safe_plugin_name(name: &str) -> bool {
!name.is_empty()
&& name.trim() == name
&& !name.chars().any(char::is_whitespace)
&& !matches!(name, "." | "..")
&& !name.contains('/') && !name.contains('\\')
} Try / catch
match plugin_target_path(&name, &plugins_dir) {
Ok(path) => path,
Err(e) if e.to_string().contains("not a safe directory name") => {
let slug: String = name.trim().to_lowercase()
.chars().map(|c| if c.is_ascii_alphanumeric() { c } else { '-' }).collect();
plugin_target_path(slug.trim_matches('-'), &plugins_dir)?
}
Err(e) => return Err(e),
} Prevention
- Author [plugin].name as a slug: lowercase, hyphens, no spaces or separators.
- Put human-readable titles in a description field, never in name.
- Add a CI lint that runs validate_skill_name_segment over published plugin names.
- Watch for invisible whitespace and BOM characters when copying names from docs.
When it happens
Trigger: Installing a plugin whose manifest [plugin].name is e.g. "my plugin" (space), "../escape", "a/b", "" or " padded "; plugin_target_path then refuses to build the target path.
Common situations: Hand-authored plugin.json with a human-readable display name in the name field; names copied from a docs headline with trailing whitespace; malicious or malformed third-party bundles attempting traversal; version differences where an older installer accepted names the current one rejects.
Related errors
- Thread summary returned HTTP ${response.statusCode}.
- Thread summaries require the runtime bearer token.
- Restore points require the runtime bearer token.
- Restore points returned HTTP ${response.statusCode}.
- parallel(): max ${MAX_ITEMS} items per call
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/f32a41dcab870cc9.
Report an issue: GitHub.