# How to Customize the OmniRoute Electron Desktop App with Plugins and Theming

> Discover how to customize the OmniRoute Electron desktop app using its plugin framework and theming capabilities. Personalize your app experience easily.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-07-19

---

**The OmniRoute Electron desktop app supports deep customization through a sandboxed plugin framework that intercepts the request pipeline and by modifying static assets in the `electron/assets/` directory to change icons, tray appearance, and window styling.**

The [diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) repository wraps a Next.js frontend in a secure Electron shell, providing multiple extension points. Customizing the OmniRoute Electron desktop app requires understanding both the **plugin system**—which runs isolated code in worker threads—and the **asset layer**, where you replace icons and window configurations to alter the visual identity.

## Architecture Overview

The application separates concerns across three layers to maintain security while enabling extensibility.

- **[`electron/main.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/main.js)** – The main process launches the Next.js server, creates the `BrowserWindow`, and manages the system tray. It implements a `waitForServer()` polling mechanism (using `stdio: 'pipe'` for log capture) and enforces a strict **Content-Security-Policy** before exposing the UI.
- **[`electron/preload.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/preload.js)** – Runs in a secure preload context with Node integration disabled and context isolation enabled. It exposes only whitelisted IPC methods (`safeInvoke`, `safeSend`, `safeOn`) to the renderer.
- **[`src/shared/hooks/useElectron.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/hooks/useElectron.ts)** – React hooks consume IPC events via `useSyncExternalStore`, enabling zero-render UI updates for states like server status and Electron environment detection.

## Extending Functionality with Plugins

The plugin engine discovers extensions from `~/.omniroute/plugins/` (or a custom path set via `OMNIROUTE_PLUGIN_PATH`), validates manifests using [`src/lib/plugins/manifest.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/manifest.ts), and executes logic inside isolated **worker threads** ([`src/lib/plugins/pluginWorker.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/pluginWorker.ts)). Plugins can register lifecycle hooks (`onRequest`, `onResponse`, `onError`) to transform traffic between the UI and AI backends.

### Creating a Type-Safe Plugin

Use the `definePlugin` helper from [`src/lib/plugins/sdk.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/sdk.ts) to ensure type safety and register hook functions. Each plugin requires a [`plugin.json`](https://github.com/diegosouzapw/OmniRoute/blob/main/plugin.json) manifest in its root directory for validation and integrity checking.

```typescript
// ~/.omniroute/plugins/welcome-banner/index.ts
import { definePlugin } from "omniroute/plugins/sdk";

export default definePlugin({
  name: "welcome-banner",
  priority: 10,
  onResponse: async (ctx, response) => {
    // Prepend a friendly banner to every chat response
    if (typeof response === "object" && "choices" in response) {
      (response as any).choices[0].message.content = 
        "👋 Welcome! " + (response as any).choices[0].message.content;
    }
    return response;
  },
});

```

The manifest ([`plugin.json`](https://github.com/diegosouzapw/OmniRoute/blob/main/plugin.json)) must include fields such as `name`, `version`, `main`, and optional `integrity` for tamper detection:

```json
{
  "name": "welcome-banner",
  "version": "1.0.0",
  "main": "index.ts",
  "integrity": "sha256-abc123..."
}

```

### Installing and Activating Plugins

Install plugins via the CLI or programmatically through the `/api/plugins/install` endpoint. The `pluginManager.loadAll()` function initializes plugins on startup, while `watcher/startWatching` enables hot-reloading when files change in the plugin directory.

```bash

# Install from a local folder

omniroute plugin install ~/.omniroute/plugins/welcome-banner

# Activate the plugin

omniroute plugin activate welcome-banner

```

### Controlling Plugin Permissions

By default, plugins cannot spawn child processes. Set the environment variable `OMNIROUTE_PLUGINS_ALLOW_EXEC=1` to grant the `exec` permission, which is required for plugins that need to run system commands. Without this flag, the sandbox terminates any process-spawning attempts for security.

## Theming the Desktop Application

Visual customization involves replacing static assets and modifying window properties in the main process.

### Replacing Application Icons

Store custom icons in `electron/assets/` using the exact filenames the packager expects:

- **`icon.ico`** – Windows application icon (256 × 256 recommended)
- **`icon.icns`** – macOS icon bundle
- **`icon.png`** – Linux/general-purpose icon (512 × 512 recommended)
- **`tray-icon.png`** – System tray icon (16 × 16 or 32 × 32 with transparency)

Rebuild the application after replacing these files to bundle the new assets.

### Modifying Window Properties

Customize the window chrome and behavior by editing [`electron/main.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/main.js). For example, use `titleBarStyle: 'hiddenInset'` on macOS to create a transparent title bar, or adjust the `backgroundColor` property to change the splash screen color during load.

```typescript
// electron/main.js
win = new BrowserWindow({
  title: "My Custom OmniRoute",
  titleBarStyle: 'hiddenInset',
  backgroundColor: '#1a1a1a',
  // ...other options
});

```

### Runtime UI Customization

The React frontend can trigger main-process actions via the whitelisted IPC channels. Use the `useElectron` hooks to safely invoke main-process methods from the renderer.

```typescript
import { useElectronAppInfo } from "src/shared/hooks/useElectron";

function ChangeTrayIconButton() {
  const setIcon = async () => {
    await window.electron.safeInvoke(
      "set-tray-icon", 
      "assets/custom-tray.png"
    );
  };
  return <button onClick={setIcon}>Use Custom Tray Icon</button>;
}

```

Since the Electron wrapper loads the built **standalone** Next.js app, any CSS changes (Tailwind or global stylesheets) in the frontend source automatically reflect in the desktop interface after rebuilding.

## Advanced Plugin Examples

### Logging Response Metadata

Create a plugin that monitors traffic without modifying it:

```typescript
// src/lib/plugins/example/log-response.ts
import { definePlugin } from "omniroute/plugins/sdk";

export default definePlugin({
  name: "log-response",
  onResponse: async (ctx, response) => {
    console.log(`[${ctx.requestId}] Response size: ${JSON.stringify(response).length}`);
    return response; // Must return the (possibly modified) response
  },
});

```

### Marketplace Integration

Browse and install remote plugins using the marketplace client defined in [`src/lib/plugins/marketplace.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/marketplace.ts), which handles searching, versioning, and secure download verification before placing files in the plugin directory.

## Summary

- **Plugin Development** – Use `definePlugin` from [`src/lib/plugins/sdk.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/sdk.ts) to create type-safe extensions that hook into `onRequest`, `onResponse`, or `onError` events within isolated worker threads.
- **Plugin Installation** – Place plugins in `~/.omniroute/plugins/` or use the CLI/API installer; control dangerous permissions via `OMNIROUTE_PLUGINS_ALLOW_EXEC`.
- **Asset Theming** – Replace icons in `electron/assets/` (`icon.ico`, `icon.icns`, `icon.png`, `tray-icon.png`) and modify [`electron/main.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/main.js) window options to rebrand the application.
- **Security Model** – The preload script ([`electron/preload.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/preload.js)) enforces context isolation, exposing only `safeInvoke`, `safeSend`, and `safeOn` to the renderer, while the plugin sandbox prevents unauthorized system access unless explicitly permitted.

## Frequently Asked Questions

### Where should I place custom plugins for the OmniRoute Electron desktop app?

Store plugins in the `~/.omniroute/plugins/` directory, or define a custom path using the `OMNIROUTE_PLUGIN_PATH` environment variable. Each plugin folder must contain a [`plugin.json`](https://github.com/diegosouzapw/OmniRoute/blob/main/plugin.json) manifest and the main entry file referenced by the `main` field.

### Is it safe to install third-party plugins?

Plugins execute inside **worker threads** ([`src/lib/plugins/pluginWorker.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/pluginWorker.ts)) with limited privileges. By default, they cannot access Node.js APIs or spawn processes. Set `OMNIROUTE_PLUGINS_ALLOW_EXEC=1` only if a specific plugin requires child process access, and verify the `integrity` hash in the manifest ([`src/lib/plugins/manifest.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/plugins/manifest.ts)) to prevent tampering.

### Can I change the tray icon without rebuilding the application?

Yes. Use the `safeInvoke` IPC method exposed by [`electron/preload.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/electron/preload.js) to send a `'set-tray-icon'` message from the renderer process, passing the path to your new icon asset. The React hook `useElectronAppInfo` from [`src/shared/hooks/useElectron.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/hooks/useElectron.ts) provides the interface for these calls.

### Does OmniRoute support hot-reloading for plugin development?

Yes. The plugin manager uses `watcher/startWatching` to monitor the plugin directory for file changes. During development, modifications to plugin code automatically trigger reloads without requiring a full application restart, though you must still re-activate the plugin if you change the manifest metadata.