How the Quickshell Plugin System Works in Omarchy: A Deep Dive into Manifest-Based Extension Architecture

Quickshell uses a manifest-driven plugin system where extensions declare their capabilities in a manifest.json file, are discovered by a PluginRegistry service, and integrate into the shell through declared entry points for bar widgets, panels, overlays, menus, and services.

The Quickshell plugin architecture powers Omarchy Quattro's extensibility, enabling both first-party bundled extensions and third-party user plugins to safely extend the desktop environment. The system centers on the PluginRegistry service in shell/services/PluginRegistry.qml, which orchestrates discovery, validation, and lifecycle management while enforcing security boundaries that prevent plugins from escaping their sandbox.

Plugin Discovery and Directory Scanning

The shell initializes plugin discovery during startup by instantiating PluginRegistry in shell/shell.qml:

property PluginRegistry pluginRegistry: PluginRegistry { }

When PluginRegistry.rescan() is invoked, it executes a Bash script that traverses two distinct directories:

  • First-party plugins: shell/plugins/ (bundled with the Omarchy repository)
  • User plugins: ~/.config/omarchy/plugins/

The scanning script emits structured blocks for each discovered manifest.json, using a delimiter format of ===<kind>::<dir>=== … === EOM ===. From PluginRegistry.qml lines 62-71:

function rescan() {
    // ...
    var script = ""
        + "emit_manifest() { ... }"
        + "scan_firstparty() { ... }"
        + "scan_thirdparty() { ... }"
        + "scan_firstparty \"$0\"; "
        + "scan_thirdparty \"$1\""
    scanProcess.command = ["bash", "-c", script,
                         registry.firstPartyDir, registry.pluginsDir]
    scanProcess.running = true
}

The parseScanOutput() method (lines 52-115) processes this output, JSON-parses each manifest, enriches it with __sourceDir and __isFirstParty metadata, and passes it to validateManifest(). Invalid manifests are rejected with warnings, and valid discoveries populate the internal installedPlugins map.

Manifest Structure and Validation

Every Quickshell plugin must provide a manifest.json conforming to schema version 1. The validateManifest() function (lines 43-89) enforces strict validation rules:

Validation Check Purpose
schemaVersion === 1 Ensures manifest compatibility with current system
Required fields (id, name, version, kinds, entryPoints) Guarantees minimal operational metadata
id as simple, non-path string Prevents filesystem traversal and namespace collisions
kinds as non-empty array Determines which shell subsystems will load the plugin
entryPoints as plain object with relative paths via isSafeEntryPoint Blocks directory escape attempts
barWidget.defaultSection ∈ {left, center, right} (optional) Controls default bar widget placement

Failed validation prints a warning and excludes the plugin from the registry.

Declaring Plugin Kinds and Entry Points

The kinds array declares which integration points a plugin provides. Valid kinds include:

  • bar — standalone bar implementations
  • bar-widget — components that embed in the main bar
  • service — shared singleton services
  • panel — toggleable side panels
  • overlay — fullscreen or semi-transparent overlays
  • menu — popup menu interfaces

Each kind specified in kinds must have a corresponding entry in entryPoints mapping to a relative QML file path.

Enabling and Disabling Plugins

Plugin activation state is managed through PluginRegistry.setEnabled(id, value, placement?) (lines 49-42). The system distinguishes between discovery and activation:

  • First-party plugins: Enabled by default in shell.json
  • Third-party plugins: Must be explicitly added to plugins[] or removed from disabledPlugins[]

The IPC handler in shell.qml lines 11-19 exposes this to external tools:

function enablePlugin(id: string, placementJson: string): string {
    try {
        var placement = JSON.parse(placementJson || "{}")
        if (shell.pluginRegistry.setEnabled(id, true, placement)) return "ok"
        return shell.pluginRegistry.lastEnableError || "unknown"
    } catch (e) {
        return "invalid placement: " + e
    }
}

Bar Widget Integration Architecture

Bar widgets receive specialized handling through the BarWidgetRegistry (shell/services/BarWidgetRegistry.qml). When a bar-widget kind plugin is enabled:

  1. syncPluginWidgets() (lines 69-179) instantiates a Component from entryPoints.barWidget
  2. The component registers via barWidgetRegistry.register(id, component, meta)
  3. The bar UI loads the widget through its sourceUrl and positions it according to defaultBarWidgetSection() or explicit placement

The convenience method putBarWidget() (lines 23-31) adds widgets idempotently—only if not already present on the bar:


# CLI equivalent: Add clock widget to right section

omarchy plugin putBarWidget example.clock '{"section":"right"}'

Repositioning uses moveBarWidget() with before or section placement directives:


# Move clock before workspaces widget

omarchy plugin moveBarWidget example.clock '{"before":"omarchy.workspaces"}'

Services, Panels, Overlays, and Menus

Beyond bar widgets, the same manifest system supports additional integration patterns:

  • Services: Singletons loaded once and shared across plugins via ensureService() / serviceFor() (around line 84)
  • Panels/Overlays/Menus: On-demand instantiation through the Instantiator in shell.qml (lines 81-154)

Entry point resolution uses entryPointUrl(manifest, kind) (lines 93-108) to construct sandboxed URLs. Each loaded component receives injected context properties:

property string omarchyPath
property var shell
property var manifest
property var barWidgetRegistry
property var pluginRegistry
property var service

Hot-Reload and Runtime Lifecycle

The Quickshell plugin system supports development workflows through Inotify-based hot-reload:

  1. File system watcher monitors ~/.config/omarchy/plugins/
  2. Changes trigger localPluginChanged() → localPluginReloadTimer (debounced)
  3. reloadPlugins() unloads active panels, services, and widgets
  4. Full rescan reconstitutes the plugin set with updated code

This cycle preserves shell stability while allowing rapid iteration on third-party plugins.

Public IPC API for External Tools

The shell exposes a minimal RPC surface via IpcHandler with target "shell" in shell.qml. These methods enable CLI and UI integration:

Method Function
listPlugins() JSON array of all plugins with status, kinds, and metadata
setPluginEnabled(id, enabled) Direct toggle of plugin activation
enablePlugin(id, placementJson) Enable with optional positioning
putBarWidget(id, placementJson) Idempotent widget placement
moveBarWidget(id, placementJson) Reorder existing widget
summon(id, payloadJson) Open panel/overlay/menu or activate bar widget
hide(id) Close panel or hide bar widget
toggle(id, payloadJson) Convenience open/close switch

CLI usage examples demonstrate typical interactions:


# Enable a third-party panel

omarchy plugin enable example.weather '{}'

# Summon network panel with custom payload

omarchy plugin summon omarchy.network '{"action":"show"}'

# List all discovered plugins

omarchy plugin list

Key Source Files and Responsibilities

File Role
shell/services/PluginRegistry.qml Core registry: scanning, validation, enable/disable, entry-point resolution
shell/shell.qml Host orchestration: registry instantiation, file watching, IPC wiring, service lifecycle
shell/services/BarWidgetRegistry.qml Bar widget component storage and metadata management
shell/services/AppLibrary.qml Application launching API for plugin consumption
Bar.qml UI container consuming BarWidgetRegistry entries (referenced from shell.qml)

Summary

  • Quickshell plugins declare capabilities through a validated manifest.json with schema version 1, supported kinds, and sandboxed entry points.
  • PluginRegistry in shell/services/PluginRegistry.qml handles discovery via Bash directory scanning, manifest validation, and enablement state management.
  • Security boundaries enforce relative entry point paths and simple plugin IDs to prevent filesystem escapes and namespace collisions.
  • Bar widgets integrate through BarWidgetRegistry with placement controls for left/center/right sections and dynamic repositioning.
  • Hot-reload via Inotify enables rapid plugin development without shell restart.
  • IPC methods exposed through shell.qml provide CLI-accessible control over the full plugin lifecycle.

Frequently Asked Questions

How does Quickshell discover third-party plugins?

Quickshell scans ~/.config/omarchy/plugins/ for manifest.json files during PluginRegistry.rescan(). The scan executes a Bash script that emits structured output for each discovery, which parseScanOutput() processes into validated plugin entries.

What prevents malicious plugins from accessing files outside their directory?

The validateManifest() function enforces isSafeEntryPoint checks that reject absolute paths or directory traversal sequences in entryPoints values. All entry points must be relative paths within the plugin's source directory, ensuring sandbox containment.

Can plugins be enabled without editing configuration files?

Yes. The enablePlugin(id, placementJson) IPC method and corresponding omarchy plugin enable CLI command programmatically activate plugins. First-party plugins start enabled by default; third-party plugins require explicit enablement through this API or manual shell.json editing.

How do I develop a plugin with live reload?

Place your plugin in ~/.config/omarchy/plugins/ during development. The shell's Inotify watcher detects changes, debounces through localPluginReloadTimer, and executes reloadPlugins() to unload and re-scan your code without restarting Quickshell.

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 →