OpenHands/OpenHands · error · Error

Installing plugins is only available on a local backend.

Error message

Installing plugins is only available on a local backend.

What it means

Thrown by PluginsManagementService.installPlugin() when the active backend is cloud. Plugin installation writes to the agent-server's local filesystem (cloning a git repo or copying a local path into the plugins directory), which only exists on a local agent-server. The guard is a hard block — there is no cloud fallback path for plugin installation.

Source

Thrown at src/api/plugins-management-service.ts:96

    if (isCloudBackend()) {
      return [];
    }

    try {
      const response = await getManagementClient().listInstalledPlugins();
      return response.plugins ?? [];
    } catch {
      // Agent-server may predate the plugins router or be unreachable; surface
      // an empty list rather than throwing (mirrors the catalog service).
      return [];
    }
  }

  static async installPlugin(
    request: InstallPluginRequest,
  ): Promise<InstalledPluginInfo> {
    if (isCloudBackend()) {
      throw new Error(
        "Installing plugins is only available on a local backend.",
      );
    }
    return getManagementClient().installPlugin(request);
  }

  static async setPluginEnabled(
    name: string,
    enabled: boolean,
  ): Promise<{ name: string; enabled: boolean }> {
    if (isCloudBackend()) {
      throw new Error(
        "Enabling and disabling plugins is only available on a local backend.",
      );
    }
    return getManagementClient().setPluginEnabled(name, enabled);
  }

View on GitHub (pinned to 500b4c533e)

Solutions

  1. Switch to a local backend before installing plugins (the Plugins page should gate the install action on backend kind === 'local').
  2. If the UI is not gating the button, ensure the component reads getActiveBackend() or useActiveBackendKind() and disables/hides the install action for cloud.
  3. Do not attempt to install plugins through a cloud runtime sandbox — plugin installation is not supported there.

Example fix

// Gate the install button on local backend
const { backend } = getActiveBackend();
<Button isDisabled={backend.kind !== 'local'} onClick={install}>
  Install
</Button>
Defensive patterns

Strategy: validation

Validate before calling

// Guard before calling installPlugin
import { getActiveBackend } from '#/api/backend-registry/active-store';

function canManagePlugins(): boolean {
  return getActiveBackend().backend.kind === 'local';
}

if (!canManagePlugins()) {
  toast.error('Switch to a local backend to install plugins.');
  return;
}

Type guard

function isLocalActiveBackend(): boolean {
  return getActiveBackend().backend.kind === 'local';
}

Try / catch

try {
  await PluginsManagementService.installPlugin(request);
} catch (error) {
  if (error instanceof Error && error.message.includes('only available on a local backend')) {
    showBackendSwitchPrompt();
  } else {
    throw error;
  }
}

Prevention

When it happens

Trigger: Any call to PluginsManagementService.installPlugin(request) while getActiveBackend().backend.kind === 'cloud'. This typically happens when the UI does not gate the 'Install' button behind a local-backend check, or when a user switches to a cloud backend mid-session and then tries to install a plugin from the Plugins page.

Common situations: The user is connected to a Cloud backend and navigates to the Plugins page, then clicks Install. Or a stale React component calls the mutation without re-checking the active backend kind after a backend switch.

Related errors


AI-assisted analysis of OpenHands/OpenHands@500b4c533e (2026-08-12). Data as JSON: /api/errors/7f53827fbeb0ea63. Report an issue: GitHub.