How Quickshell Plugins Are Declared and Managed in Omarchy

Quickshell plugins in the Omarchy desktop environment use a manifest-driven registry system where each plugin directory contains a manifest.json file declaring its identity, supported kinds, and entry points, all managed at runtime by the PluginRegistry component in shell/services/PluginRegistry.qml.

The Quickshell plugin architecture in basecamp/omarchy centers on a declarative manifest system that enables dynamic discovery, validation, and lifecycle management of both first-party bundled extensions and third-party user plugins. At startup, the shell instantiates a centralized PluginRegistry that scans designated directories, validates manifest schemas, and exposes helper methods for enabling, disabling, and positioning plugins within the desktop shell.

Plugin Manifest Structure

Every Quickshell plugin is defined by a manifest.json file located in the plugin's root directory. This manifest declares the plugin's unique identifier, version, supported "kinds" (such as service, bar-widget, or panel), and entry-point file paths.

A minimal panel plugin manifest looks like this:

{
  "id": "example.panel",
  "name": "Example Panel",
  "version": "1.0.0",
  "schemaVersion": 1,
  "kinds": ["panel"],
  "entryPoints": {
    "service": "Service.qml"
  }
}

The kinds array determines what types of shell components the plugin provides, while entryPoints maps each kind to its corresponding QML implementation file. For bar widgets, you may optionally include a barWidget.defaultSection field (left, center, or right) to specify initial placement.

Plugin Discovery and Registry Initialization

When the shell initializes in shell.qml, it creates an instance of PluginRegistry and wires it to the shell configuration:

property PluginRegistry pluginRegistry: PluginRegistry { }
...
pluginRegistry.firstPartyDir = shell.firstPartyPluginsDir
pluginRegistry.shellConfigProvider = function() { return shell.shellConfig }
pluginRegistry.shellConfigMutator = function(mutate) { shell.mutateShellConfig(mutate) }
pluginRegistry.rescan()

The rescan() method triggers a scan of two distinct locations:

  • First-party plugins bundled with Omarchy (located under shell/plugins/…)
  • Third-party plugins installed by the user in ~/.config/omarchy/plugins

Scanning is performed by a Bash script that emits raw manifest JSON surrounded by markers. The PluginRegistry.parseScanOutput function processes this output, while validateManifest() enforces schema compliance and security constraints before storing valid plugins in the installedPlugins map.

Plugin Lifecycle Management

Enabling and Disabling Plugins

Plugin state is persisted in shell.json. The registry provides isEnabled(id), setEnabled(id, value, placement), and related helpers to manipulate this configuration. When you enable a plugin, setEnabled invokes the wired shellConfigMutator to add entries to the plugins[] array or update bar configurations.

To enable a plugin programmatically:

if (shell.pluginRegistry) {
  var ok = shell.pluginRegistry.setEnabled("example.panel", true)
  console.log(ok ? "Enabled" : "Failed")
}

Disabling follows the same pattern:

shell.pluginRegistry.setEnabled("example.panel", false)

Bar Widget Placement

For plugins of kind bar-widget, the registry provides putBarWidget(id, placement) and moveBarEntry to manage layout positioning. The manifest may specify a barWidget.defaultSection field, but you can override this when enabling the widget.

A bar widget manifest includes placement metadata:

{
  "id": "example.widget",
  "name": "Example Widget",
  "version": "1.0.0",
  "schemaVersion": 1,
  "kinds": ["bar-widget"],
  "entryPoints": {
    "barWidget": "BarWidget.qml"
  },
  "barWidget": {
    "defaultSection": "right"
  }
}

To place the widget after a specific element:

var placement = { after: "omarchy.tray" }
shell.pluginRegistry.putBarWidget("example.widget", placement)

Security and Validation

Before a plugin enters the installedPlugins map, validateManifest() performs several security checks:

  • Verifies the schema version matches the expected format
  • Ensures required fields (id, name, version, kinds) are present
  • Confirms entry-point paths are safe and remain within the plugin's source directory via entryPointUrl(manifest, kind)
  • Blocks plugin IDs that conflict with the reserved omarchy.* namespace

The entryPointUrl function builds file URLs only after confirming the resolved path does not escape the plugin directory, preventing directory traversal attacks.

Configuration Integration

The registry uses dependency injection for configuration access via shellConfigProvider and shellConfigMutator. These callbacks allow PluginRegistry to read and write the canonical shell.json without performing direct file I/O, ensuring consistency with the shell's state management.

All plugin operations increment registryRevision to trigger UI refreshes, while signals such as pluginsChanged, pluginLoadFailed, and localPluginChanged notify other components of state changes.

Summary

  • Quickshell plugins require a manifest.json declaring the plugin ID, version, kinds, and entry points.
  • The PluginRegistry in shell/services/PluginRegistry.qml manages discovery, validation, and lifecycle across first-party and third-party plugin directories.
  • rescan() executes a Bash script to discover manifests, while validateManifest() enforces security constraints and schema compliance.
  • Enabling and disabling plugins modifies shell.json through the setEnabled helper and its wired mutator functions.
  • Bar widgets support positional placement via putBarWidget and barTarget, with default sections defined in the manifest.
  • The registry prevents unsafe path resolution and reserves the omarchy.* namespace for built-in components.

Frequently Asked Questions

How do I install a third-party Quickshell plugin in Omarchy?

Place the plugin directory containing a valid manifest.json into ~/.config/omarchy/plugins/. Run rescan() or restart the shell to discover the new plugin. The PluginRegistry will automatically validate the manifest and add it to the installedPlugins map, making it available for enabling via the shell configuration.

What fields are required in a Quickshell plugin manifest?

A valid manifest must include id, name, version, schemaVersion, and kinds. The entryPoints object must map each declared kind to a relative file path. For bar-widget kinds, you may optionally include barWidget.defaultSection to specify initial placement (left, center, or right).

How does the PluginRegistry prevent malicious plugins from accessing files outside their directory?

The registry validates entry-point paths through entryPointUrl(manifest, kind), which resolves the absolute path and confirms it remains within the plugin's source directory. This prevents directory traversal attacks. Additionally, validateManifest() blocks plugin IDs using the reserved omarchy.* namespace and checks that all file references are safe before allowing the plugin to load.

Can I programmatically move a bar widget after it has been enabled?

Yes. Use shell.pluginRegistry.moveBarEntry(id, placement) or putBarWidget(id, placement) to reposition widgets within the bar layout. The placement object accepts properties like after or before to specify relative positioning, and the registry updates both the UI state and the underlying shell.json configuration automatically.

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 →