ruvnet/ruflo · error
Invalid package name
Error message
Invalid package name: ${spec} What it means
The plugin manager validates npm package specs against VALID_PACKAGE_RE before shelling out to npm (the S-3 shell-injection guard): an optional lowercase @scope/ prefix, a name starting with [a-z0-9-~] and continuing with [a-z0-9-._~], plus an optional @version/dist-tag suffix (@[a-z0-9._\-^~>=<]+). Anything else — uppercase letters, spaces, quotes, command-substitution characters — throws 'Invalid package name'. All plugin install/enable paths run this check.
Solutions
- Use a plain npm spec: 'name', '@scope/name', 'name@1.2.3', or 'name@latest' — all lowercase, no spaces.
- For local development, publish to a registry or use a file: path only if the install path you call supports it (this validator does not — check installPlugin's accepted inputs).
- If the name came from user/LLM input, normalize it (lowercase, trim, strip metacharacters) before validation.
- Treat a rejection of obviously injectable input as the guard working — inspect where the string came from.
Example fix
# before claude-flow plugins install "My Plugin@latest" # after claude-flow plugins install my-plugin@latest
Defensive patterns
Strategy: validation
Validate before calling
const VALID_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\-^~>=<]+)?$/;
function assertPluginSpec(spec: string): void {
if (!VALID_PACKAGE_RE.test(spec)) {
throw new Error(`Invalid plugin package spec: ${spec} — use lowercase npm name, optional @scope/ and @version`);
}
}
assertPluginSpec(userSpec); // before plugins install Type guard
function isValidPluginSpec(spec: unknown): spec is string {
return typeof spec === 'string' &&
/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\-^~>=<]+)?$/.test(spec);
} Try / catch
try {
await pluginsInstall(spec);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Invalid package name:')) {
const cleaned = spec.trim().toLowerCase().replace(/\s+/g, '-');
if (cleaned !== spec && isValidPluginSpec(cleaned)) return pluginsInstall(cleaned);
throw new UserInputError(`'${spec}' is not a valid npm package spec`);
}
throw e;
} Prevention
- Normalize plugin names (trim, lowercase, strip quotes) before passing them to the manager.
- Only accept registry specs (name, @scope/name, name@version) — reject URLs and local paths at your UI boundary.
- Remember the regex is an injection guard: never 'fix' a rejected spec by loosening the pattern.
When it happens
Trigger: plugins install with specs like 'My-Plugin' (uppercase), 'my plugin' (space), 'pkg@1.0.0 || 2' (bad version range chars), 'pkg; rm -rf ~' (injection attempt), or a URL/git spec, which this regex deliberately does not accept.
Common situations: Typing plugin names with capitals or spaces; passing git URLs or local paths where only registry name[@version] is supported; malicious or mangled input from automated tooling being (correctly) rejected; dist-tag expressions using characters outside the allowed @suffix set.
Related errors
- Command not allowed
- AIDefence installed but failed to load
- AIDefence package not available. Install with: npm install…
- basePath contains disallowed characters
- Dangerous key segment rejected
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/861bec185a090874.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/plugins/manager.ts:35
// supported invocation is via a real `.exe` shell. We wrap every npm call
// through `cmd.exe /d /s /c npm <args>`, which keeps Node's safe array-form
// argument escaping intact and avoids both ENOENT and EINVAL.
const isWindows = process.platform === 'win32';
function runNpm(args: string[], timeoutMs: number): Promise<{ stdout: string; stderr: string }> {
if (isWindows) {
return execFileAsync('cmd.exe', ['/d', '/s', '/c', 'npm', ...args], { timeout: timeoutMs });
}
return execFileAsync('npm', args, { timeout: timeoutMs });
}
/**
* Validate npm package name to prevent shell injection (S-3)
*/
const VALID_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\-^~>=<]+)?$/;
function validatePackageName(spec: string): void {
if (!VALID_PACKAGE_RE.test(spec)) {
throw new Error(`Invalid package name: ${spec}`);
}
}
// ============================================================================
// Types
// ============================================================================
export interface InstalledPlugin {
name: string;
version: string;
installedAt: string;
enabled: boolean;
source: 'npm' | 'local' | 'ipfs';
path?: string;
commands?: string[];
hooks?: string[];
config?: Record<string, unknown>;
}View on GitHub (pinned to fa13ee4ad6)