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

  1. Use a plain npm spec: 'name', '@scope/name', 'name@1.2.3', or 'name@latest' — all lowercase, no spaces.
  2. 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).
  3. If the name came from user/LLM input, normalize it (lowercase, trim, strip metacharacters) before validation.
  4. 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

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


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)