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 implementationsbar-widget— components that embed in the main barservice— shared singleton servicespanel— toggleable side panelsoverlay— fullscreen or semi-transparent overlaysmenu— 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 fromdisabledPlugins[]
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:
syncPluginWidgets()(lines 69-179) instantiates aComponentfromentryPoints.barWidget- The component registers via
barWidgetRegistry.register(id, component, meta) - The bar UI loads the widget through its
sourceUrland positions it according todefaultBarWidgetSection()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:
- File system watcher monitors
~/.config/omarchy/plugins/ - Changes trigger
localPluginChanged()→localPluginReloadTimer(debounced) reloadPlugins()unloads active panels, services, and widgets- 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.jsonwith schema version 1, supported kinds, and sandboxed entry points. - PluginRegistry in
shell/services/PluginRegistry.qmlhandles 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
BarWidgetRegistrywith 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.qmlprovide 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →