How Omarchy Handles Shell Plugins: Discovery, Validation, and Activation

Omarchy manages shell plugins through a centralized PluginRegistry that scans user and system directories, validates manifest.json files for security, and activates only explicitly enabled extensions via property injection.

Omarchy is a custom desktop shell built by Basecamp that enables deep customization through a secure plugin architecture. Understanding how Omarchy handles shell plugins requires examining the PluginRegistry service instantiated in shell.qml that orchestrates discovery, validation, and runtime activation without requiring full session restarts.

Plugin Discovery and Directory Scanning

Omarchy discovers plugins at startup by scanning two distinct locations. The ShellRoot creates a PluginRegistry object as a property in shell.qml (property PluginRegistry pluginRegistry: PluginRegistry { }), which immediately calls pluginRegistry.rescan() from within Component.onCompleted.

The registry scans:

  • User plugins – Located at $HOME/.config/omarchy/plugins (referenced as pluginsDir), created automatically on first run via PluginRegistry.ensureUserDir().
  • First-party plugins – Bundled within the application's shell/plugins directory (referenced as firstPartyDir).

This dual-path approach allows both system-wide and user-specific extensions to coexist safely.

Manifest Validation and Security Checks

Every plugin must contain a manifest.json describing its structure. The registry loads each JSON file and executes validateManifest() to enforce strict security policies before activation.

According to the Omarchy source code in shell/services/PluginRegistry.qml, validation requires:

  • schemaVersion === 1 with required fields present (id, name, version, kinds, entryPoints).
  • Safe plugin IDs – No slashes or .. sequences allowed to prevent directory traversal.
  • Non-empty kinds array – Must specify at least one plugin type (e.g., service, panel, bar).
  • Safe entry points – Paths must pass isSafeEntryPoint checks to ensure they remain relative.

Warnings emit via console.warn for any validation failures, preventing malformed or suspicious plugins from loading.

Entry Point Resolution and Sandboxing

Once validated, the registry resolves executable entry points through entryPointUrl(manifest, kind). This method constructs a file: URL for the requested plugin type while enforcing filesystem sandbox boundaries.

The function double-checks that resolved absolute paths remain within the plugin's source directory, effectively preventing sandbox escape attacks where a malicious manifest might attempt to access sensitive system files outside its containment folder.

Enablement and Configuration Logic

Activation depends on the shell configuration stored in ~/.config/omarchy/shell.json. The PluginRegistry.isEnabled(id) method implements a tiered policy:

  • Bar widgets – The plugin ID must match the selected bar configuration's bar.id property.
  • Standard plugins – The ID must appear in shell.json → plugins[] array unless it is a first-party infrastructure plugin (implicitly enabled).
  • Disabled plugins – Any ID listed in disabledPlugins[] is explicitly blocked regardless of other settings.

This configuration-driven approach ensures administrators and users maintain granular control over which code executes in the shell environment.

Runtime Integration and Hot Reloading

Omarchy avoids singleton anti-patterns by injecting shared services directly into plugin components. When instantiating a plugin, the shell sets properties for pluginRegistry, barWidgetRegistry, and appLibrary, allowing components to access system services without global state.

The architecture supports hot-reloading through Qt's signal system. When shell.json is edited or plugin files are added or removed, PluginRegistry emits pluginsChanged and scanFinished, triggering the shell to refresh its plugin list without terminating the session.

Creating a Custom Omarchy Shell Plugin

To create a plugin that Omarchy recognizes and loads safely, you need three components: a valid manifest, a configuration entry, and the implementation file.

1. Define the Manifest

Create ~/.config/omarchy/plugins/my-plugin/manifest.json:

{
  "schemaVersion": 1,
  "id": "my.plugin",
  "name": "My Plugin",
  "version": "0.1.0",
  "kinds": ["service"],
  "entryPoints": {
    "service": "Service.qml"
  }
}

This structure matches the schema validated by PluginRegistry.validateManifest() in the source code.

2. Register in Shell Configuration

Add the plugin ID to ~/.config/omarchy/shell.json:

{
  "version": 1,
  "plugins": ["my.plugin"]
}

The plugins array is consulted by PluginRegistry.isEnabled() to determine activation.

3. Implement the Service Component

Create ~/.config/omarchy/plugins/my-plugin/Service.qml:

import QtQuick
import Quickshell

QtObject {
  // Injected automatically by ShellRoot
  property PluginRegistry pluginRegistry: null

  Component.onCompleted: {
    console.log("Plugin loaded; accessing config:", pluginRegistry.shellConfigProvider())
  }
}

The pluginRegistry property receives its value through injection when the shell instantiates the component, avoiding tight coupling to global state.

Summary

  • Discovery – PluginRegistry.rescan() triggers at startup via Component.onCompleted in shell.qml, checking both user (~/.config/omarchy/plugins) and bundled (shell/plugins) directories.
  • Validation – validateManifest() enforces schemaVersion, safe IDs, and path constraints before loading any code.
  • Security – entryPointUrl() prevents directory traversal by ensuring resolved paths stay within the plugin's root folder.
  • Activation – isEnabled() checks shell.json configuration, allowing explicit enablement via the plugins array or implicit loading for infrastructure components.
  • Runtime – Services inject via properties rather than singletons, with hot-reloading supported through pluginsChanged signals.

Frequently Asked Questions

Where does Omarchy look for shell plugins?

Omarchy scans two locations at startup: the user directory at $HOME/.config/omarchy/plugins and the bundled first-party directory at shell/plugins. The registry creates the user directory automatically via ensureUserDir() if it does not exist on first run.

What fields are required in an Omarchy plugin manifest?

A valid manifest.json must include schemaVersion (set to 1), id (unique identifier without slashes), name, version, kinds (array of types like service or panel), and entryPoints (object mapping kinds to relative file paths). The validateManifest() function in PluginRegistry.qml rejects any manifest missing these fields or containing unsafe path characters.

How does Omarchy prevent malicious plugins from escaping their directory?

The registry implements multiple safeguards: validateManifest() blocks IDs containing .. or slashes, and entryPointUrl() validates that resolved absolute paths remain within the plugin's source directory before returning a file: URL. This layered approach prevents path traversal attacks that might otherwise access sensitive system files.

Can I develop Omarchy plugins without restarting the shell?

Yes. The PluginRegistry emits pluginsChanged and scanFinished signals whenever shell.json is modified or plugin files are added or removed. The shell listens for these signals to refresh the plugin list dynamically, enabling hot-reloading of extensions without terminating your desktop session.

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 →