ruvnet/ruflo · error · PluginError

DUPLICATE_PLUGIN

DUPLICATE_PLUGIN

Error message

Plugin '${plugin.name}' is already loaded

What it means

PluginLoader.loadPlugin(plugin, context) first validates the plugin interface, then registers it by unique name; if the registry already has a plugin with the same name it throws PluginError with code 'DUPLICATE_PLUGIN' before any initialization runs (PluginError carries pluginName and code fields). Loading the same plugin object - or a different object with the same name - twice on one loader is rejected.

Source

Thrown at v3/@claude-flow/shared/src/plugin-loader.ts:99

  private registry: PluginRegistry;
  private initializationOrder: string[] = [];
  private healthCheckIntervalId?: NodeJS.Timeout;

  constructor(registry: PluginRegistry, config?: PluginLoaderConfig) {
    this.registry = registry;
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Load a single plugin
   */
  async loadPlugin(plugin: ClaudeFlowPlugin, context: PluginContext): Promise<void> {
    // Validate plugin
    this.validatePlugin(plugin);

    // Check for duplicates
    if (this.registry.hasPlugin(plugin.name)) {
      throw new PluginError(
        `Plugin '${plugin.name}' is already loaded`,
        plugin.name,
        'DUPLICATE_PLUGIN'
      );
    }

    // Register plugin in uninitialized state
    this.registry.registerPlugin(plugin, 'uninitialized', context);

    // Resolve dependencies
    if (this.config.strictDependencies) {
      this.validateDependencies(plugin);
    }

    // Initialize plugin
    await this.initializePlugin(plugin, context);

    // Update initialization order

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. Deduplicate the list by name before loading: [...new Map(plugins.map(p => [p.name, p])).values()]
  2. Make plugin loading single-shot by memoizing the loadPlugins call
  3. If overlap is expected, catch PluginError with code 'DUPLICATE_PLUGIN' and treat that specific plugin as already-loaded success

Example fix

// before
await loader.loadPlugins([authPlugin, authPlugin]); // second entry throws

// after
const unique = [...new Map(plugins.map(p => [p.name, p])).values()];
await loader.loadPlugins(unique);
Defensive patterns

Strategy: validation

Validate before calling

// dedupe by name before loading - registry keys are plugin names
const uniquePlugins = [...new Map(plugins.map(p => [p.name, p])).values()];
await loader.loadPlugins(uniquePlugins);

Try / catch

import { PluginError } from '@claude-flow/shared/dist/plugin-interface';
try {
  await loader.loadPlugin(plugin, ctx);
} catch (e) {
  if (e instanceof PluginError && e.code === 'DUPLICATE_PLUGIN') {
    return; // same plugin already registered - treat as idempotent
  }
  throw e;
}

Prevention

When it happens

Trigger: loadPlugins([p, p]) with the same plugin listed twice; two entries registering 'my-plugin' (a local copy and an npm copy); calling loadPlugin again in retry code after a partial success; HMR re-running the registration module with the same plugin definitions.

Common situations: Monorepos bundling the same plugin twice; re-entrant init code; plugin name collisions between teams; tests reusing a loader across cases.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/06b9912d43182893. Report an issue: GitHub.