How the Omarchy Plugin System Works: Architecture and Implementation Guide

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 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 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 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.

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 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:

// ~/.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:

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

Loading an entry point with safety checks:

// 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:

// 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →