# How the Omarchy Plugin System Works: Architecture and Implementation Guide

> Discover the Omarchy plugin system architecture. Learn how this manifest-driven framework loads QML services, widgets, and menus while ensuring secure sandboxed paths.

- Repository: [37signals/omarchy](https://github.com/basecamp/omarchy)
- Tags: architecture
- Published: 2026-08-23

---

**The Omarchy plugin system is a manifest-driven framework where `PluginRegistry.qml` discovers extensions from both bundled and user directories, loading QML-based services, bar widgets, and menus while enforcing sandboxed paths and reserved first-party IDs.**

The Omarchy desktop environment, developed by Basecamp, implements a flexible plugin architecture within its Quickshell-based shell. Understanding how the Omarchy plugin system works is essential for developers extending the desktop with custom widgets or background services. The system centers on a central registry that dynamically loads components based on declarative JSON manifests and strict security validations.

## Plugin Discovery and Registration

### Directory Scanning Strategy

At initialization, the `PluginRegistry` defined in [`shell/services/PluginRegistry.qml`](https://github.com/basecamp/omarchy/blob/quattro/shell/services/PluginRegistry.qml) scans two distinct locations. **First-party plugins** bundled with the application reside under `shell/plugins/`, while **user-installed extensions** live in `$HOME/.config/omarchy/plugins`. The registry maintains a `scanProcess` that monitors these directories for changes, particularly watching the user plugin folder for filesystem events around line 692.

### Manifest Parsing

Each plugin must provide a [`manifest.json`](https://github.com/basecamp/omarchy/blob/main/manifest.json) file that declares its identity and capabilities. During the scan, the registry parses these manifests into a map structure of `{ pluginId: manifest }` (see lines 23-25 of `PluginRegistry.qml`). This mapping enables constant-time lookups when resolving dependencies or loading specific entry points.

## Entry Points and Component Types

### Bar Widgets

Plugins can contribute visual elements to the Quickshell bar by declaring a `barWidget` entry in their manifest. The [`BarWidgetRegistry.qml`](https://github.com/basecamp/omarchy/blob/quattro/shell/services/BarWidgetRegistry.qml) handles the instantiation of these components, typically referencing the widget path around line 4. When loaded, the widget QML file gains access to the plugin's context and resources.

### Background Services

Long-running functionality such as nightlight management or notification handling is implemented through **service plugins**. Declared under the `service` key in the manifest, these components are loaded via the `loadEntryPoint` function (line 80 of `PluginRegistry.qml`). This method performs path validation before creating the QML component instance.

### Menu Extensions

The system also supports dynamic menu entries and custom panels through the menu plugin interface, managed by `MenuModel.qml`. These extensions integrate with the global application menu, allowing plugins to expose actions and submenus without modifying core shell code.

## Lifecycle Management and Security

### Enabling and Disabling Plugins

Plugin activation is controlled through the user's [`shell.json`](https://github.com/basecamp/omarchy/blob/main/shell.json) configuration file, which contains a `plugins` array. Each entry may be a simple string ID or an object with additional parameters. When `PluginRegistry.setEnabled(id, true)` is invoked, the registry updates the configuration (lines 517 and 532), either appending the plugin to the active list or removing it to disable functionality.

### Sandboxing and Path Validation

Security is enforced through path validation within `loadEntryPoint`. Before loading any resource, the registry checks that the requested path resides within the plugin's designated `__sourceDir` using `Util.isSafePath`. This prevents plugins from accessing files outside their sandboxed directory, mitigating directory traversal attacks.

### Reserved ID Protection

First-party plugin IDs are reserved to prevent shadowing of critical system components. If a third-party plugin attempts to register using a reserved identifier, the registry emits a warning at line 604 and rejects the registration attempt, ensuring that user plugins cannot override built-in functionality.

### Hot-Reloading for Development

During development, the registry monitors the user plugins directory for file changes via the `localPluginChanged` handler at line 650. When modifications are detected, the affected plugin is unloaded and reloaded without requiring a full shell restart, enabling rapid iteration of extension code.

### Error Handling

When plugin loading fails, the system emits the `pluginLoadFailed` signal (line 31), providing error details that the UI layer can surface to users. This signal includes information about the specific entry point that failed validation or instantiation.

## Implementation Examples

Creating a weather widget plugin:

```json
// ~/.config/omarchy/plugins/weather/manifest.json
{
  "id": "custom.weather",
  "description": "Displays current weather conditions",
  "service": "WeatherService.qml",
  "barWidget": "WeatherWidget.qml"
}

```

Enabling the plugin in shell configuration:

```json
// ~/.config/omarchy/shell.json
{
  "plugins": [
    { "id": "custom.weather" }
  ],
  "bar": {
    "layout": [
      { "widget": "custom.weather" }
    ]
  }
}

```

Loading an entry point with safety checks:

```qml
// Excerpt from shell/services/PluginRegistry.qml
function loadEntryPoint(plugin, entry) {
    var source = plugin.__sourceDir + "/" + entry;
    if (!Util.isSafePath(source, plugin.__sourceDir)) {
        console.warn("PluginRegistry: invalid entry point " + source);
        return null;
    }
    return Qt.createComponent(source);
}

```

Registering a widget instance:

```qml
// Conceptual usage within BarWidgetRegistry.qml
function registerWidget(pluginId, widgetPath) {
    var plugin = PluginRegistry.pluginById(pluginId);
    var component = PluginRegistry.loadEntryPoint(plugin, widgetPath);
    if (component.status === Component.Ready) {
        var instance = component.createObject(barContainer);
        barWidgets[pluginId] = instance;
    }
}

```

## Summary

- The Omarchy plugin system uses a central `PluginRegistry.qml` to coordinate loading of first-party and user-installed extensions from standardized directory locations.
- Plugins declare their capabilities through JSON manifests that specify entry points for bar widgets, background services, and menu items.
- Security is maintained through strict path validation using `Util.isSafePath`, reserved ID protection for system plugins, and sandboxing via `__sourceDir`.
- Configuration management occurs through [`shell.json`](https://github.com/basecamp/omarchy/blob/main/shell.json), allowing users to enable or disable specific plugins via the `setEnabled` method.
- Development is supported through hot-reloading capabilities that detect filesystem changes and reload plugins without restarting the shell.

## Frequently Asked Questions

### Where does Omarchy look for user-installed plugins?

Omarchy scans the `$HOME/.config/omarchy/plugins` directory for user plugins, while first-party bundled plugins reside under `shell/plugins/` within the application installation. The registry monitors both locations, with particular emphasis on watching the user directory for changes to enable hot-reloading.

### What happens if two plugins have the same ID?

The registry enforces ID uniqueness with reserved protection for first-party plugins. If a third-party plugin attempts to use an ID reserved for system components, the registry emits a warning at line 604 and rejects the registration. For conflicts between user plugins, the first loaded instance typically takes precedence, though this behavior is considered undefined and should be avoided.

### How does Omarchy prevent plugins from accessing files outside their directory?

Before loading any QML component or resource, the `loadEntryPoint` function validates the requested path against the plugin's `__sourceDir` using `Util.isSafePath`. This check ensures that relative path traversal (e.g., `../../etc/passwd`) is blocked, confining the plugin to its own sandboxed directory.

### Can I modify a plugin while Omarchy is running?

Yes. The plugin system implements hot-reloading through the `localPluginChanged` handler at line 650 of `PluginRegistry.qml`. When you modify files within `$HOME/.config/omarchy/plugins/`, the registry detects the change, unloads the existing plugin instance, and reloads the updated version without requiring a full shell restart.