How Plugins Work in the Omarchy Quickshell Desktop: Architecture and Implementation

TLDR: Omarchy uses a file-based plugin system where JSON manifests define entry points, PluginRegistry.qml discovers and validates extensions, and BarWidgetRegistry.qml manages bar widgets, with activation state controlled through shell.json configuration.

The Omarchy desktop environment extends Quickshell with a modular architecture that separates core functionality from community extensions. Understanding how plugins work in the Omarchy Quickshell desktop reveals a security-first design that validates all third-party code before execution while supporting dynamic configuration changes. The system relies on two primary registries and a strict manifest schema to balance flexibility with sandboxed safety.

Plugin Discovery and Manifest Validation

When the desktop initializes, shell/services/PluginRegistry.qml instantiates and scans two distinct directories for available extensions. The registry distinguishes between first-party plugins bundled in the repository via firstPartyDir and user-installed plugins located at ~/.config/omarchy/plugins via pluginsDir.

Directory Structure and Plugin Types

Each plugin directory contains subfolders with a mandatory manifest.json file conforming to schema version 1. According to the basecamp/omarchy source code, the registry processes all discovered manifests through the validateManifest() function located at lines 43-90 in PluginRegistry.qml. This validation ensures required fields including id, name, version, kinds, and entryPoints are present and correctly formatted.

Security Validation and Path Sanitization

The validation logic enforces several safety constraints. The id must be a safe string containing no slashes or .. sequences, while kinds must be a non-empty array specifying the plugin type such as "service", "panel", or "bar". The isSafeEntryPoint() helper at lines 36-40 verifies that all entry-point paths remain relative and stay within the plugin's source directory, preventing directory traversal attacks. Valid manifests are stored in the installedPlugins property as a keyed object.

Resolving Entry Points and Security Boundaries

Plugins expose functionality through entry points defined in their manifests, such as "service" mapping to Service.qml. The entryPointUrl(manifest, kind) function at lines 93-108 constructs a file:// URL for the requested component. This method performs a defense-in-depth check to ensure the resolved path does not escape the plugin's sandbox directory before returning the URL for dynamic loading.

Enabling and Disabling Plugins via Configuration

A plugin becomes active only when referenced in the user's shell.json configuration file. The isEnabled(id) method at lines 110-138 implements the activation logic with kind-specific rules.

Bar Plugin Activation

For bar plugins, the config.bar.id value must match the plugin's identifier exactly. Other plugin kinds activate when appearing in bar.layout.* for bar widgets or the plugins[] array for panels, overlays, and services.

First-Party Plugin Defaults

First-party plugins receive special treatment: they are implicitly enabled unless explicitly listed in the disabledPlugins[] array, as implemented at lines 135-137. The findEntryLocation() function traverses the configuration to locate plugin references, enabling the registry to emit pluginsChanged signals when the active set modifications occur.

Bar Widget Registration Architecture

Bar widgets represent a specialized plugin category that renders on the top bar. These components register through shell/services/BarWidgetRegistry.qml, which maintains a widgets map storing each widget's component and metadata. Plugins invoke the register(id, component, metadata) function at lines 16-25 to announce their widgets:

// Example: Registering a Clock bar widget
import QtQuick 2.15
import Quickshell 1.0

Component {
    id: clockWidget
    
    // Widget implementation...
    
    Component.onCompleted: {
        BarWidgetRegistry.register("omarchy.clock", clockWidget, {
            defaultSection: "right",
            label: "Clock"
        })
    }
}

The registry tracks a revision number to notify the bar of additions or removals, enabling reactive UI updates without full restarts.

Runtime Component Loading and Wiring

When instantiating a plugin, the shell requests the entry-point URL from PluginRegistry and loads it via Qt.createComponent(). This pattern appears throughout the plugin architecture, from service implementations to panel loaders:

// Example: Loading the Night Light service plugin
import QtQuick 2.15
import Quickshell 1.0

QtObject {
    Component.onCompleted: {
        const url = PluginRegistry.entryPointUrl(
            PluginRegistry.installedPlugins["omarchy.nightlight"], 
            "service"
        )
        if (url) {
            const comp = Qt.createComponent(url)
            if (comp.status === Component.Ready) {
                comp.createObject(null)
            } else {
                console.warn("Failed to load NightLight service:", comp.errorString())
            }
        }
    }
}

Integration with shell.qml

The shell.qml file at lines 18-22 wires the registry into the application context by injecting shellConfigProvider and shellConfigMutator properties. This dependency injection allows PluginRegistry to read configuration state and persist changes via setEnabled() without directly handling filesystem I/O. When users enable or disable plugins, the registry updates shell.json through the mutator and triggers a re-scan of the bar layout.

Plugin Lifecycle and Events

The plugin system follows a strict lifecycle: Scan, Enable/Disable, and Reload. After startup, the registry emits scanFinished once both bundled and user directories are processed. Configuration changes trigger setEnabled() calls that modify shell.json and refresh the active plugin set. If a developer modifies a plugin's manifest during runtime, the registry reloads the plugin and emits pluginLoadFailed on validation errors, providing immediate feedback without crashing the desktop.

Summary

  • Discovery: PluginRegistry.qml scans firstPartyDir and ~/.config/omarchy/plugins for manifest.json files, validating them via validateManifest() at lines 43-90.
  • Security: isSafeEntryPoint() and entryPointUrl() enforce directory sandboxing and prevent path traversal attacks.
  • Activation: Plugins enable via shell.json entries, with first-party plugins active by default unless listed in disabledPlugins[].
  • Bar Widgets: BarWidgetRegistry.qml manages widget registration with reactive revision tracking via register().
  • Lifecycle: Events like scanFinished, pluginsChanged, and pluginLoadFailed provide hooks for reactive UI updates.

Frequently Asked Questions

How does Omarchy validate plugin safety before loading?

Omarchy validates plugins through multiple layers in PluginRegistry.qml. The validateManifest() function checks schema compliance and safe ID strings, while isSafeEntryPoint() ensures all entry-point paths remain within the plugin directory. Additionally, entryPointUrl() performs runtime path resolution checks at lines 93-108 before returning file:// URLs for component loading.

Where should I place custom plugins for the Omarchy desktop?

User-installed plugins belong in ~/.config/omarchy/plugins, scanned via the pluginsDir property in PluginRegistry.qml. First-party plugins bundled with the repository reside in a separate firstPartyDir. Both locations require a valid manifest.json with schema version 1 and properly defined entry points.

How do I disable a built-in first-party plugin?

First-party plugins remain enabled by default unless explicitly disabled. Add the plugin's ID to the disabledPlugins[] array in your shell.json configuration. The isEnabled() logic at lines 135-137 checks this array before implicitly activating bundled extensions.

What is the difference between a service plugin and a bar widget?

Service plugins provide background functionality without UI elements, loaded via their Service.qml entry point and activated through the plugins[] array. Bar widgets are visual components that register through BarWidgetRegistry.register() at lines 16-25 and appear on the top bar via bar.layout.* configuration sections.

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 →