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

> Learn how Omarchy manages shell plugins. It discovers, validates, and activates extensions securely through a central PluginRegistry, ensuring only enabled plugins run.

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

---

**Omarchy manages shell plugins through a centralized `PluginRegistry` that scans user and system directories, validates [`manifest.json`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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`:

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

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

```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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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`](https://github.com/basecamp/omarchy/blob/main/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.