{"record":{"id":"861bec185a090874","repo":"ruvnet/ruflo","slug":"invalid-package-name-spec","errorCode":null,"errorMessage":"Invalid package name: ${spec}","messagePattern":"Invalid package name: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/plugins/manager.ts","lineNumber":35,"sourceCode":"// supported invocation is via a real `.exe` shell. We wrap every npm call\n// through `cmd.exe /d /s /c npm <args>`, which keeps Node's safe array-form\n// argument escaping intact and avoids both ENOENT and EINVAL.\nconst isWindows = process.platform === 'win32';\n\nfunction runNpm(args: string[], timeoutMs: number): Promise<{ stdout: string; stderr: string }> {\n  if (isWindows) {\n    return execFileAsync('cmd.exe', ['/d', '/s', '/c', 'npm', ...args], { timeout: timeoutMs });\n  }\n  return execFileAsync('npm', args, { timeout: timeoutMs });\n}\n\n/**\n * Validate npm package name to prevent shell injection (S-3)\n */\nconst VALID_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\\-^~>=<]+)?$/;\nfunction validatePackageName(spec: string): void {\n  if (!VALID_PACKAGE_RE.test(spec)) {\n    throw new Error(`Invalid package name: ${spec}`);\n  }\n}\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface InstalledPlugin {\n  name: string;\n  version: string;\n  installedAt: string;\n  enabled: boolean;\n  source: 'npm' | 'local' | 'ipfs';\n  path?: string;\n  commands?: string[];\n  hooks?: string[];\n  config?: Record<string, unknown>;\n}","sourceCodeStart":17,"sourceCodeEnd":53,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/plugins/manager.ts#L17-L53","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nclaude-flow plugins install \"My Plugin@latest\"\n\n# after\nclaude-flow plugins install my-plugin@latest","handlingStrategy":"validation","validationCode":"const VALID_PACKAGE_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\\-^~>=<]+)?$/;\nfunction assertPluginSpec(spec: string): void {\n  if (!VALID_PACKAGE_RE.test(spec)) {\n    throw new Error(`Invalid plugin package spec: ${spec} — use lowercase npm name, optional @scope/ and @version`);\n  }\n}\nassertPluginSpec(userSpec); // before plugins install","typeGuard":"function isValidPluginSpec(spec: unknown): spec is string {\n  return typeof spec === 'string' &&\n    /^(@[a-z0-9-~][a-z0-9-._~]*\\/)?[a-z0-9-~][a-z0-9-._~]*(@[a-z0-9._\\-^~>=<]+)?$/.test(spec);\n}","tryCatchPattern":"try {\n  await pluginsInstall(spec);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Invalid package name:')) {\n    const cleaned = spec.trim().toLowerCase().replace(/\\s+/g, '-');\n    if (cleaned !== spec && isValidPluginSpec(cleaned)) return pluginsInstall(cleaned);\n    throw new UserInputError(`'${spec}' is not a valid npm package spec`);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["plugins","npm","validation","security","shell-injection"],"backgroundTag":"invalid-npm-package-name","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}