How the Quickshell Plugin System Works in Omarchy: Architecture and Runtime Flow
Omarchy’s Quickshell plugin system uses a dynamic registry architecture where PluginRegistry.qml scans manifest files, BarWidgetRegistry.qml manages UI components, and shell.qml coordinates runtime loading to enable hot-swapping extensions without restarting the compositor.
The Omarchy desktop environment leverages Quickshell, a lightweight QML-based compositor, to power its extensible shell architecture. At the heart of this system lies a three-service design that separates plugin discovery from widget registration and runtime coordination. Understanding these boundaries reveals how Omarchy maintains stability while allowing users to install, enable, and reposition third-party extensions at runtime.
Plugin Discovery and Manifest Validation
Every plugin interaction begins with a strict validation pipeline that ensures only compliant extensions enter the runtime.
Directory Structure and Plugin Locations
Plugins reside in two distinct namespaces based on their origin. First-party plugins ship with Omarchy under shell/plugins/ (for example, panels/audio), while third-party plugins live in user space at ~/.config/omarchy/plugins/<plugin-id>/. This separation allows the registry to apply different trust boundaries and update policies to system versus user extensions.
Manifest Schema and Validation
Each plugin must declare a manifest.json (or *.manifest.json) conforming to schemaVersion 1. The manifest specifies the plugin’s identity, capabilities, and entry points:
{
"schemaVersion": 1,
"id": "omarchy.audio",
"name": "Audio",
"version": "1.0",
"kinds": ["bar-widget"],
"entryPoints": { "bar-widget": "panels/audio/Panel.qml" }
}
The PluginRegistry.qml validates these manifests through validateManifest(), which checks required fields, ensures the plugin ID contains no path traversal characters, verifies that kinds is a non-empty array, and confirms every entry point uses a relative path. Validated manifests populate the installedPlugins map in shell/services/PluginRegistry.qml (lines 43-90).
The Scanning Process
At startup, shell.qml invokes pluginRegistry.rescan() to enumerate available extensions. The registry constructs a Bash script that walks both the first-party directory and the user-plugin directory, emitting each manifest in a delimited format (===<kind>::<absolute-source-dir>=== … === EOM ===). A Process object executes this script, and PluginRegistry.parseScanOutput() parses the stdout to build the internal plugin catalogue (lines 452-511).
Enabling and Disabling Plugins at Runtime
The Quickshell plugin system mutates configuration state through injected helper functions rather than direct file access.
Configuration Management
shell.qml injects two critical dependencies into PluginRegistry:
pluginRegistry.shellConfigProvider = function() { return shell.shellConfig }
pluginRegistry.shellConfigMutator = function(mut) { shell.mutateShellConfig(mut) }
The setEnabled(id, value, placement) method (lines 104-132) serves as the central toggle routine. It adds or removes entries from config.plugins[], manages the disabledPlugins[] array for built-in defaults the user turns off, and handles special cases such as the omarchy.bar option plugin. All mutations flow through shellConfigMutator, ensuring the ~/.config/omarchy/shell.json file stays synchronized with the in-memory state.
Bar Widget Placement
When a plugin declares kinds: ["bar-widget"], enabling it triggers automatic layout insertion. The registry uses barTarget() and moveBarEntry() helpers to position the widget within config.bar.layout.<section>. If the manifest specifies barWidget.defaultSection, the system respects that hint; otherwise, it defaults to the center section of the bar.
Plugin Cloning and Conflicts
Omarchy supports plugin cloning for user customizations. When a cloned plugin activates, the registry tracks the relationship via manifest.omarchy.clonedFrom. The activeCloneFor() function ensures mutual exclusivity—enabling a clone automatically disables its source using restoreCloneSource(), and vice versa. This prevents configuration conflicts when users iterate on third-party widgets (lines 190-214).
Bar Widget Registration
While PluginRegistry handles metadata, BarWidgetRegistry.qml manages the live UI components. Instantiated once by shell.qml as property BarWidgetRegistry barWidgetRegistry: BarWidgetRegistry { }, this service maintains the widgets map and a revision counter.
Plugins register their components at runtime:
Component {
id: myWidget
// Widget implementation
}
Component.onCompleted: {
widgetRegistry.register("my.custom.widget", myWidget, { title: "My Widget" })
}
Each registration increments revision and emits the changed signal, causing shell/plugins/bar/Bar.qml to re-evaluate barWidgetRegistry.availableIds() and dynamically load new components without restarting Quickshell.
Hot Reloading and File Watching
The system achieves zero-downtime updates through inotifywait integration. A background Process named localPluginWatcher monitors ~/.config/omarchy/plugins/ for filesystem events. When changes occur, localPluginChanged(pluginId) sets pluginReloadPending and starts a 150 ms debounce timer (localPluginReloadTimer). Upon expiration, the timer triggers shell.reloadPlugins(), which executes rescan() and applies updated manifests while preserving the current shell session (lines 363-382).
Summary
PluginRegistry.qmlorchestrates discovery, validation, and enable/disable logic throughrescan()andsetEnabled().BarWidgetRegistry.qmlprovides the runtime catalogue for UI components viaregister()and revision-based change notifications.- Manifest validation enforces schemaVersion 1, prevents directory traversal in entry points, and validates plugin IDs.
- Configuration state persists to
~/.config/omarchy/shell.jsonthrough mutator functions injected byshell.qml. - Hot reloading uses
inotifywaitand a 150 ms debounce timer to apply plugin changes without compositor restarts.
Frequently Asked Questions
What is the required schema version for Omarchy plugin manifests?
Omarchy requires schemaVersion 1 for all plugin manifests. This version mandates fields including id, name, version, kinds, and entryPoints. The validateManifest() function in PluginRegistry.qml rejects any manifest lacking these fields or containing non-relative paths in entry points.
How does Omarchy handle third-party vs first-party plugins?
First-party plugins reside in shell/plugins/ and ship with the system, while third-party plugins install to ~/.config/omarchy/plugins/<plugin-id>/. The scanning process treats both directories identically for validation, but first-party plugins receive special handling in the disabledPlugins[] array when users opt to turn off built-in defaults.
Can plugins be enabled without restarting the Quickshell compositor?
Yes. The setEnabled() method mutates the live configuration object and bumps the registryRevision, emitting pluginsChanged to signal the shell to reload affected components. For filesystem changes, the localPluginWatcher process detects modifications and triggers shell.reloadPlugins() after a 150 ms debounce, achieving hot-reload without session interruption.
What prevents directory traversal attacks in plugin entry points?
The validateManifest() function explicitly checks that all paths in entryPoints are relative and contain no parent directory references (..). This validation occurs before any plugin enters the installedPlugins map, ensuring the QML engine never loads files outside the plugin’s designated directory.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →