# How the Quickshell Plugin System Works in Omarchy: Architecture and Runtime Flow

> Discover Omarchy's Quickshell plugin system architecture and runtime flow. Learn how dynamic registries and hot-swapping enable seamless extension loading without compositor restarts.

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

---

**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`](https://github.com/basecamp/omarchy/blob/main/manifest.json) (or `*.manifest.json`) conforming to **schemaVersion 1**. The manifest specifies the plugin’s identity, capabilities, and entry points:

```json
{
  "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`:

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

```qml
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.qml`** orchestrates discovery, validation, and enable/disable logic through `rescan()` and `setEnabled()`.
- **`BarWidgetRegistry.qml`** provides the runtime catalogue for UI components via `register()` 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.json` through mutator functions injected by `shell.qml`.
- **Hot reloading** uses `inotifywait` and 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.