# Building Your Own Omarchy Plugin

> Build, test, and publish an Omarchy plugin: manifest and layout, the clone-edit-validate loop, wiring into the bar, menu, keybindings and theme, the safety rules, and listing it on the marketplace.


---

# Building Your Own Omarchy Plugin

You have a small itch: a widget the bar does not have, a panel that shows the one thing you check forty times a day. On Omarchy 4 you do not fork the desktop to scratch it. You copy a built-in plugin into your own folder, change it, and the desktop reloads it as you save.

This guide walks the full path: what a plugin is made of, the development loop the official docs describe, how it hooks into the menu, keys, and theme, the safety rules, and how to publish. Checked against Omarchy 4.0.4.

## Prerequisite

Read [Omarchy Plugins and the Marketplace](/guides/omarchy-plugins-and-the-marketplace) first. It explains the trust model that the safety rules in Phase 3 build on. You also need a terminal comfortable enough for editing files and running commands ([The Terminal and Shell](/guides/the-terminal-and-shell)) and basic git ([Git From Zero](/guides/git-from-zero)), since a plugin is a git repo.

Plugin UI code is QML, the declarative language of Qt Quick that Quickshell uses. This guide teaches the plugin structure and workflow, not QML itself: you will start from working built-in code and change it.

## How to read this

- **Want a working widget fast?** Phase 2 is the loop, and it starts from a built-in clone.
- **Planning to share it?** Do not skip Phases 3 and 4. Safety and publishing are where plugins get rejected or cause harm.

## The phases

1. **[Anatomy of a Plugin](01-anatomy-of-a-plugin.md)** - the manifest, the six kinds, entry points, and what a plugin repository looks like.
2. **[The Development Loop](02-the-development-loop.md)** - clone a built-in, edit, validate, run, and read the logs when it misbehaves.
3. **[Integrating With the Desktop, Safely](03-integrating-with-the-desktop-safely.md)** - bar placement, summoning, keybindings, menu rows, theme tokens, launching apps, and the safety rules.
4. **[Testing and Publishing](04-testing-and-publishing.md)** - a pre-flight checklist, the permanent id, and listing on plugins.omarchy.org.

Sibling guides: [Making Omarchy Comfortable](/guides/making-omarchy-comfortable) for `bindings.lua`, and [When Omarchy Breaks](/guides/when-omarchy-breaks) if an experiment leaves the desktop in a bad state.


---

# Anatomy of a Plugin

Before you change anything, it helps to know what the shell looks for when it loads a plugin folder. The answer is short: one JSON file that describes the plugin, and the files that file points to. Every failure you will hit in the next phase, from "not listed" to "does nothing", traces back to this contract.

## The manifest

A plugin is a directory with a `manifest.json` and some QML. When you publish, the directory is a git repository with `manifest.json` at its root. Here is the complete development manifest from the official guide, for a clock widget:

```json
{
  "schemaVersion": 1,
  "id": "yourname.clock",
  "name": "Custom Clock",
  "version": "1.0.0",
  "author": "Your name",
  "license": "MIT",
  "description": "A small clock with a details panel for the Omarchy bar.",
  "kinds": ["bar-widget"],
  "entryPoints": { "barWidget": "BarWidget.qml" },
  "barWidget": {
    "displayName": "Custom Clock",
    "category": "Time",
    "allowMultiple": false,
    "defaultSection": "center"
  },
  "omarchy": { "clonedFrom": "omarchy.clock" }
}
```

| Field | What it does |
|---|---|
| `schemaVersion` | Must be exactly the number `1`. The string `"1"` is rejected. |
| `id` | Unique, namespaced identifier. Letters, digits, dots, dashes, underscores; must start with a letter or digit; no `..`; never starts with `omarchy.`. |
| `name` | Human-readable name. |
| `version` | Your version string. The marketplace shows up to 64 characters. |
| `author` | Shown in the marketplace. |
| `license` | Present in the official examples. Publishing requires a license anyway. |
| `description` | Short summary shown in the marketplace. |
| `kinds` | Non-empty array of what the plugin is. |
| `entryPoints` | Object mapping each kind to the QML file the shell loads. |
| `barWidget` | Extra block for bar widgets: `displayName`, `category`, `allowMultiple`, `defaultSection`, plus optional `defaults` and a `schema` list of settings. |
| `keepLoaded` | When `true`, keeps the plugin mounted between summons. |
| `omarchy.clonedFrom` | Development only. Remove before publishing. |

The local validator and the marketplace ask for slightly different fields. The validator insists on `schemaVersion`, `id`, `name`, `version`, `kinds`, and `entryPoints`. The marketplace listing additionally needs `author` and `description`. Fill in all of them from the start.

## Kinds and entry points

Each kind you claim needs a matching key in `entryPoints`. The official guide gives the usual file name for each:

| Kind | `entryPoints` key | Conventional file | Use it for |
|---|---|---|---|
| `bar-widget` | `barWidget` | `BarWidget.qml` | An item in the active bar |
| `panel` | `panel` | `Panel.qml` | A floating surface |
| `overlay` | `overlay` | `Overlay.qml` | A fullscreen surface |
| `menu` | `menu` | `Menu.qml` | A summoned menu |
| `service` | `service` | `Service.qml` | A headless singleton |
| `bar` | `bar` | `Bar.qml` | A full bar replacement |

The file name is a convention, not a rule: the shell README's minimal example uses `Widget.qml` for a bar widget. What is a rule is that every path is relative, contains no `..`, and points at a file that exists. A plugin can declare several kinds at once, as the built-in media plugin does.

One subtlety: the clock in the official guide has a details panel, but its manifest declares only `bar-widget`. `BarWidget.qml` loads `Panel.qml` itself with a `Loader`, so a nested panel does not need its own manifest kind.

```mermaid
flowchart LR
  M["manifest.json"] -->|"entryPoints.barWidget"| B["BarWidget.qml"]
  B -->|"Loader"| P["Panel.qml"]
  B --> J["Model.js (helpers)"]
```

## A real repository

Here is the layout of a real multi-kind plugin, the Missing Manual plugin from the previous guide ([omarchy-tmm](https://github.com/Topurrra/omarchy-tmm)). Its manifest declares `panel`, `service`, and `bar-widget`, plus `keepLoaded: true`:

```text
manifest.json
Panel.qml
Controller.qml
BarWidget.qml
Reader.qml
ResultList.qml
Service.qml
Markdown.js
Model.js
components/            small shared QML pieces (with a qmldir)
logo.png
bin/                   helper scripts the plugin runs
bindings.lua.fragment  keybinding lines for the user to append
extensions/omarchy-menu.jsonc   menu rows for the user to merge
README.md
INSTALL.md
LICENSE
```

Read it as a pattern, not a template. Entry points are at the top; supporting QML and JavaScript sit beside them; a plugin can ship helper scripts in `bin/`; and the two "fragment" files are things the user opts into, because a plugin installer never edits your config for you.

Four rules shape what a folder may contain:

- No symlinks anywhere inside the folder (the `.git` directory is skipped). A symlink could point a copied plugin back at arbitrary files on disk.
- Entry point paths must be safe relative paths.
- A plugin never needs a build step the installer runs for it. The installer clones and validates; it does not run anything from your repo.
- Document every external dependency. The tmm repo lists its requirements (`curl`, `python3`, optional `wl-copy`) in its README.

Try the manifest syntax yourself:

```exercise
[
  {
    "type": "json",
    "task": "Write the `kinds` and `entryPoints` part of a manifest as a JSON object for a plugin that is only a floating panel, using the conventional file name. Type a JSON object with exactly two keys: \"kinds\" and \"entryPoints\".",
    "expected": { "kinds": ["panel"], "entryPoints": { "panel": "Panel.qml" } },
    "hint": "kinds is an array of strings, and entryPoints maps the kind's key (panel) to a QML file name."
  }
]
```

Check yourself before moving on:

```quiz
[
  {
    "q": "omarchy plugin validate rejects your plugin with: kind 'panel' requires an 'entryPoints.panel' to load. What is wrong?",
    "choices": [
      "Panel.qml does not exist on disk",
      "The manifest claims the panel kind but has no panel key under entryPoints",
      "The id uses a reserved namespace"
    ],
    "answer": 1,
    "explain": "Each claimed kind needs its own key in entryPoints. A missing file gives a different message: entry point file not found.",
    "why": ["A missing file is reported as entry point file not found.", null, "A reserved id gives a message about the reserved omarchy.* namespace."]
  },
  {
    "q": "Which plugin id will validate?",
    "choices": ["omarchy.mywidget", "io.github.alex.tide-chart", "../widget"],
    "answer": 1,
    "explain": "The omarchy. prefix is reserved and anything with .. is invalid. A namespaced id such as io.github.alex.tide-chart is fine."
  },
  {
    "q": "Your bar widget shows a details panel when clicked. Do you need to declare panel in kinds?",
    "choices": [
      "Yes, every Panel.qml needs its own kind",
      "No, if BarWidget.qml loads Panel.qml itself, the bar-widget kind is enough"
    ],
    "answer": 1,
    "explain": "The official clock example keeps kinds as [\"bar-widget\"] and has BarWidget.qml load Panel.qml through a Loader."
  }
]
```

## Recap

1. A plugin is a folder with `manifest.json` plus the files it points to; publishing means a git repo with the manifest at its root.
2. The validator requires `schemaVersion` (the number 1), `id`, `name`, `version`, `kinds`, and `entryPoints`; the marketplace also needs `author` and `description`.
3. Each kind needs its matching `entryPoints` key, and every entry path must be relative, free of `..`, and exist on disk.
4. A bar widget's nested panel does not need its own kind if the widget loads it.
5. Plugin folders may not contain symlinks, and ids may not start with `omarchy.`.

Next up, [The Development Loop](02-the-development-loop.md): clone a working built-in and watch your edits reload live.


---

# The Development Loop

The fastest way to learn a plugin system is to start from one that works. Omarchy builds that in: you clone a built-in into your own folder, and from that moment the desktop runs your copy and reloads it whenever you save. This phase is the loop you will repeat dozens of times: edit, validate, run, inspect.

## When it breaks first

| Symptom | Calm fix |
|---|---|
| Plugin folder not found | Use the exact id printed by `omarchy plugin clone`; confirm the folder under `~/.config/omarchy/plugins/`. |
| `entry point file not found` | Match the `entryPoints` value to the file name and capitalization on disk. |
| Validates but is not listed | `omarchy-shell shell rescanPlugins`, then `omarchy plugin list --json`. |
| Listed but does not appear | Enable it, confirm the declared kind, and read the shell log (below). |
| Panel opens once, never again | Forward `opened`, `open()`, and `close()` from the bar entry point to the loaded panel. |

Those are the official troubleshooting entries. The rest of the phase explains where each comes from.

## Step 1: clone a built-in

Choose a built-in with the same kind and interaction pattern as what you want to build. For a bar widget with a details panel, that is the built-in clock:

```bash
omarchy plugin clone omarchy.clock --edit
```

*What just happened:* Omarchy printed a new plugin id built from your username, for example `yourname.clock`, created `~/.config/omarchy/plugins/yourname.clock/`, opened the folder in your editor, and replaced the built-in clock in your active bar with your copy. The folder holds `manifest.json`, `BarWidget.qml`, `Panel.qml`, and `Model.js`.

The whole plugin directory is copied, including every declared kind and local dependency. Calls made to the original id (`omarchy.clock`) are routed to your clone, so nothing that referred to the built-in needs changing. If you make a mess, `omarchy plugin remove yourname.clock` puts the built-in back, because the clone keeps `omarchy.clonedFrom` in its manifest while you develop.

> ⚠️ **Gotcha.** Never edit the built-in under `$OMARCHY_PATH`. It belongs to the package, and the next update overwrites it. Always work in your own copy under `~/.config/omarchy/plugins/`.

Keep the clone id while developing. You choose a permanent one at publish time (Phase 4).

## Step 2: edit with live reload

Saving any file under `~/.config/omarchy/plugins/` reloads the plugin automatically, so leave the editor open and watch changes land. If discovery seems stale, force it:

```bash
omarchy-shell shell rescanPlugins
```

Here is the heart of the cloned `BarWidget.qml`, abridged from the official example. The manifest points at this file; it draws the clock button and loads `Panel.qml`:

```qml
import QtQuick
import Quickshell
import qs.Ui

BarWidget {
  id: root
  moduleName: "yourname.clock"

  // ... open(), close(), toggle() forward to the loaded panel ...

  SystemClock {
    id: clock
    precision: SystemClock.Minutes
  }

  Loader {
    id: panelLoader
    active: true
    source: Qt.resolvedUrl("Panel.qml")
    visible: false
  }

  WidgetButton {
    id: button
    anchors.fill: parent
    bar: root.bar
    text: Qt.formatTime(clock.date, "HH:mm")
    tooltipText: "Open Custom Clock"
    onPressed: function(buttonCode) {
      if (buttonCode === Qt.LeftButton) root.toggle()
    }
  }
}
```

Two details matter. `moduleName` must be the same in `BarWidget.qml` and `Panel.qml` (the official example sets it to the plugin id). And the imports come from the shell: `qs.Ui` for the bar and panel building blocks and `qs.Commons` for the shared `Style` and `Color` tokens. Take the full working files from your clone rather than from this excerpt.

To start without a clone, make the folder by hand: put `manifest.json` and your QML in `~/.config/omarchy/plugins/<plugin-id>/`, run `omarchy-shell shell rescanPlugins`, then `omarchy plugin enable <id>`. Bar widgets land in `barWidget.defaultSection`, or in the center when it is omitted, and you can move them later with `omarchy bar move`.

## Step 3: validate, without running

Two checks, neither of which runs your code:

```bash
PLUGIN_ID="yourname.clock"
PLUGIN_DIR="$HOME/.config/omarchy/plugins/$PLUGIN_ID"
omarchy plugin validate "$PLUGIN_DIR"
qmllint -I "$OMARCHY_PATH/shell" \
  "$PLUGIN_DIR/BarWidget.qml" "$PLUGIN_DIR/Panel.qml"
```

`omarchy plugin validate` checks the manifest and layout using the same rules the shell enforces at load time. `qmllint` checks your QML against the shell's installed imports. Both should exit without an error. A broken mapping gives an actionable message:

```console
$ omarchy plugin validate "$PLUGIN_DIR"
omarchy-plugin-validate: entry point file not found: 'BarWidget.qml'
```

*What just happened:* the validator found that `entryPoints` names a file that is not on disk. It checks, in order, that the manifest is valid JSON, that `schemaVersion` is the number 1, that the required fields exist, that the id is legal and not reserved, that `kinds` is a non-empty array, that every entry point is a safe relative path to a real file, that each claimed kind has its key, and that there are no symlinks.

## Step 4: run and inspect

The clone command already enabled your plugin. Confirm the shell sees it:

```bash
omarchy plugin list --json \
  | jq --arg id "$PLUGIN_ID" '.[] | select(.id == $id)'
```

The result should include your id, its kind, and `"enabled": true`. Then drive the panel through the shell's IPC, which is the same route a keybinding or menu entry uses:

```bash
omarchy-shell shell summon "$PLUGIN_ID" '{}'
omarchy-shell shell hide "$PLUGIN_ID"
```

`summon` prints `ok` when the shell knows the plugin and `unknown` when it does not. A panel closes with `Escape` too.

## Reading the logs

When a plugin is listed and enabled but nothing appears, the QML probably failed to draw. The shell logs QML errors, and two documented ways to read them are:

```bash
qs log -p "$OMARCHY_PATH/shell" --tail 100
journalctl -t omarchy-shell -n 100 --no-pager
```

The first comes from the official development guide; the second is the journal tag the shell logs under, as used in the omarchy-tmm README. Add `-f` to the journal command to watch live while you summon. If a reload does not seem to take, `omarchy restart shell` restarts the shell. One more rule from the shell docs: a plugin marked `keepLoaded: true` stays mounted, so code changes to a kept-loaded service itself only take effect after a shell restart.

> 🪖 **War story.** The silent failure the validator exists to prevent: a manifest that claims a kind without its entry point would otherwise install, enable, and do nothing, explained only by one line on the shell console. The validator's own comments say it refuses this while there is still someone to tell. Run it every time.

Check yourself before moving on:

```quiz
[
  {
    "q": "You edited Panel.qml in your clone and saved. What must you do to see the change?",
    "choices": [
      "Log out and back in",
      "Nothing: saving a file under ~/.config/omarchy/plugins/ reloads the plugin; rescanPlugins forces it if needed",
      "Run omarchy plugin add again"
    ],
    "answer": 1,
    "explain": "Live reload is the point of working in your own plugin folder. Use omarchy-shell shell rescanPlugins when discovery looks stale."
  },
  {
    "q": "omarchy plugin validate passes, omarchy plugin list shows your plugin enabled, but the widget is not on screen. Where do you look next?",
    "choices": [
      "The shell log, for QML errors, using qs log or journalctl -t omarchy-shell",
      "The marketplace",
      "Delete the manifest and recreate it"
    ],
    "answer": 0,
    "explain": "Validation covers the manifest and layout, not whether your QML runs. A QML error shows up in the shell log."
  },
  {
    "q": "Why is it unsafe to edit the built-in clock under $OMARCHY_PATH directly?",
    "choices": [
      "It is read-only forever",
      "Those files belong to the package, and the next update overwrites your changes",
      "Omarchy refuses to start with edited built-ins"
    ],
    "answer": 1,
    "explain": "That is why omarchy plugin clone copies the plugin into your own config directory first."
  }
]
```

## Recap

1. `omarchy plugin clone omarchy.clock --edit` copies a working built-in into `~/.config/omarchy/plugins/<yourname>.clock/`, enables it, and swaps it into your bar.
2. Saving files there reloads the plugin; `omarchy-shell shell rescanPlugins` forces discovery.
3. `omarchy plugin validate <folder>` and `qmllint -I "$OMARCHY_PATH/shell" <files>` check the plugin without running it.
4. `omarchy plugin list --json | jq` confirms it is enabled; `omarchy-shell shell summon <id> '{}'` and `hide <id>` exercise it.
5. When it validates but does not show, read the log: `qs log -p "$OMARCHY_PATH/shell" --tail 100` or `journalctl -t omarchy-shell`.

Next up, [Integrating With the Desktop, Safely](03-integrating-with-the-desktop-safely.md): bar placement, keys, menu rows, theme, and the rules that keep a plugin from hurting anyone.


---

# Integrating With the Desktop, Safely

A plugin that only draws is half a plugin. People will want a key to open it, a row in the menu, colors that follow their theme, and a shortcut to launch a related app. Each of those touches the reader's own configuration, so this phase is as much about restraint as capability: the plugin offers, the user opts in.

## Placing it in the bar

A `bar-widget` declares where it would like to land through `barWidget.defaultSection`, which must be `left`, `center`, or `right`. The validator rejects anything else. After enabling, the user can move it:

```bash
omarchy bar put tmm.manual --section right
omarchy bar move io.github.yourname.custom-clock --section center
```

Both forms appear in official docs: `put` in the omarchy-tmm README for a widget that did not appear on its own, and `move` in the development guide's README template. Settings for a widget are stored inline on its entry in `~/.config/omarchy/shell.json`. A widget that makes sense twice on one bar sets `allowMultiple: true`; most set it to `false`.

## Summoning over IPC

You control a running plugin from outside through one wrapper, `omarchy-shell`, which forwards calls to the running shell:

| Call | Effect |
|---|---|
| `omarchy-shell shell summon <id> '<payloadJson>'` | Load and open a panel or overlay. Prints `ok` or `unknown`. |
| `omarchy-shell shell toggle <id> '<payloadJson>'` | Summon if closed, hide if open. |
| `omarchy-shell shell hide <id>` | Close it. |
| `omarchy-shell shell call <id> <method> <arg>` | Call a method on an already-loaded plugin. |
| `omarchy-shell shell rescanPlugins` | Re-walk plugin folders and hot-reload code. |

The payload is JSON that your plugin defines the meaning of. The Missing Manual plugin accepts keys like `query`, `slug`, `catalog`, and `random`:

```bash
omarchy-shell shell summon tmm.manual '{"query":"git rebase"}'
```

Design your payload the way you would design a tiny API, because keybindings, menu rows, and scripts will all call it. The cheap way to make every entry route consistent is the same trick tmm uses: every menu row and keybinding summons the window through this one IPC call, so all of them land in the same place.

## Keybindings: offer a fragment

Omarchy 4 Hyprland config is Lua. The user's `~/.config/hypr/bindings.lua` is theirs, and your plugin should not edit it. Instead, ship a fragment the user can append, as tmm does:

```lua
local o = require("default.hypr.helpers")
o.bind("SUPER + ALT + M", "Missing Manual", "omarchy-shell shell toggle tmm.manual")
```

`o.bind(keys, description, command)` is Omarchy's helper: the description shows up in the keybinding viewer, and the command runs when the keys fire. The viewer is `omarchy menu keybindings`, also on `Super + K`, and `omarchy menu keybindings --print` lists the live bindings. Check it before you pick a key, so you do not shadow a default.

The helper also accepts a table in place of a command string for common launches, per its source: `{ tui = "btop" }` runs a terminal app (with `focus = true` to focus an existing window instead of opening a second), and `{ webapp = "https://..." }` opens a web app. Plain strings are shell commands.

## Menu rows: merge, never overwrite

The Omarchy menu has your own extension file, `~/.config/omarchy/extensions/omarchy-menu.jsonc`. Entries are keyed by dotted id, and the id places them in the tree: `personal` is a top-level row and `personal.notes` appears inside it. From the manual:

```jsonc
{
  "personal": {"icon": "", "label": "Personal"},
  "personal.notes": {"icon": "󰎞", "label": "Notes", "action": "omarchy-launch-editor ~/notes"}
}
```

Two practical facts. The `icon` is drawn literally as text, so it is a Nerd Font glyph and not an icon name: a name like `search` would show up as the word search. And the file is shared by every user menu entry, so a plugin that copies a fragment over it deletes the reader's other entries. tmm solves this with a helper script, `tmm-menu`, that merges its rows in, keeps a `.bak`, refuses to write anything that does not parse, and can remove its own rows. After any change, `omarchy menu refresh` reloads the menu.

## Following the theme

The reason your plugin should have no hardcoded colors is that Omarchy themes change them. The official clock reads colors and sizes from shared tokens: `Style` for spacing and fonts (`Style.space(240)`, `Style.font.subtitle`) and `Color` from `qs.Commons`. tmm goes further: its README says surfaces use the `Color.menu.*` tokens, the corner radius follows Hyprland's rounding, and spacing follows the shell's font and spacing settings, so a theme switch repaints the whole window. Follow the tokens and your plugin looks native in every theme.

## Launching things

When your plugin needs to start something, prefer Omarchy's own launchers over reinventing them. The documented ones include `omarchy launch tui <command>` (a TUI in the default terminal with Omarchy styling), `omarchy launch or focus tui <command>` (focus an existing one), `omarchy launch webapp <url>`, and `omarchy launch editor <path>`. They exist so everything Omarchy opens looks and behaves the same.

## Scoped, not sandboxed

Third-party plugins get capability-scoped facades rather than the real host objects. A full replacement bar, for example, gets detached snapshots and narrow proxies, not live service objects. Do not design a plugin that needs more reach than that, and do not assume the scoping protects the user from you: the shell README calls facades API boundaries, not same-process sandboxes.

## Safety rules

The official development guide opens with the rule: plugins run unsandboxed with your user permissions, so review every dependency and command, avoid unnecessary privileges, and never start a second Quickshell process for a plugin. Here is the full working set, with what comes from official docs and what is our advice:

**Official:**

- Avoid unnecessary privileges. The installer never uses `sudo`, and your plugin should not need it.
- No install hooks. The installer never runs plugin code, so do not rely on a post-install step. Anything the plugin needs must be a step the reader runs on purpose, documented in the README.
- Never start a second Quickshell process.
- No symlinks in the folder.
- Document every external dependency, setup step, privilege boundary, service, installer, or remote build.

**Our advice, based on how tmm is built:**

- Bound everything you call out to. tmm routes every network request through a helper, `bin/tmm-cap`, that caps the reply size and the run time and kills the process group past either.
- Send nothing about the user you do not have to. tmm sends only the search query or the question, and its README says so.
- Keep secrets in the user's own config, not your repo. tmm's optional AI chat reads a bring-your-own-key file at `~/.config/tmm/ai.json`.
- Make every setup step reversible, and ship the undo: tmm's README has a full uninstall list, because `omarchy plugin remove` only deletes the plugin's own folder.
- Keep optional features optional, and say what each does when its dependency is missing (tmm falls back to a plain card when `rsvg-convert` is absent).

> 💡 **Key point.** The marketplace validates listings, not plugin security. The safety work is yours, and the people reading your code before they enable it are your real audience.

Check yourself before moving on:

```quiz
[
  {
    "q": "Your plugin needs a keybinding. What is the right way to deliver it?",
    "choices": [
      "Write to ~/.config/hypr/bindings.lua from the plugin on first load",
      "Ship a bindings.lua fragment and document that the user appends it, so the change is their decision",
      "Ask for sudo and edit the system Hyprland files"
    ],
    "answer": 1,
    "explain": "bindings.lua is the user's file. A fragment the reader appends keeps them in control, which is what tmm does.",
    "why": ["Editing a user's config unasked is exactly the kind of surprise to avoid.", null, "Plugins must never need sudo, and system files are not where user bindings live."]
  },
  {
    "q": "Why does tmm ship tmm-menu instead of telling users to cp its menu file into place?",
    "choices": [
      "The menu file is binary",
      "The extension file is shared by all user menu entries, so cp would delete the user's other rows; the helper merges and keeps a backup",
      "Menu files must be created by root"
    ],
    "answer": 1,
    "explain": "One file holds every user menu entry. Merging protects them; omarchy menu refresh then reloads the menu."
  },
  {
    "q": "Which of these is an official rule for plugin authors?",
    "choices": [
      "Start a dedicated Quickshell process for each plugin to isolate it",
      "Never start a second Quickshell process for a plugin",
      "Run helper scripts with sudo so they can reach system services"
    ],
    "answer": 1,
    "explain": "Plugins live in the one long-running shell process. Starting another is explicitly ruled out."
  }
]
```

## Recap

1. `barWidget.defaultSection` must be `left`, `center`, or `right`; users move widgets with `omarchy bar move` or `omarchy bar put`.
2. Keybindings, menu rows, and scripts all reach your plugin through `omarchy-shell shell summon/toggle/hide`, with a JSON payload you design.
3. Deliver keybindings as a fragment for `bindings.lua`, and menu rows by merging into `~/.config/omarchy/extensions/omarchy-menu.jsonc`; never overwrite either file.
4. Use the shell's `Style` and `Color` tokens so themes repaint your plugin; use `omarchy launch ...` to start apps and TUIs.
5. Safety: no `sudo`, no install hooks, no second Quickshell process, no symlinks, document every dependency, and bound and minimize whatever you call out to.

Next up, [Testing and Publishing](04-testing-and-publishing.md): the pre-flight checklist and the marketplace submission.


---

# Testing and Publishing

A plugin that works on your machine, in your clone, with your settings is a draft. Publishing means strangers will install it into a process that runs as them. This phase is the pre-flight list, the id change that turns a clone into a real plugin, and the three-step marketplace submission.

## Test like a stranger will use it

The official development guide lists what to test before sharing. Run through all of it:

1. **Click** the widget, and use every control in the panel.
2. **Escape** closes the panel.
3. **Shell open and close** work: `omarchy-shell shell summon <id> '{}'` and `omarchy-shell shell hide <id>`.
4. **Disable** it with `omarchy plugin disable <id>`, then **re-enable** it.
5. **Restart the shell** with `omarchy restart shell` and confirm it comes back.
6. **Removal** with `omarchy plugin remove <id>` leaves the desktop intact. For a clone, the built-in returns.

Add three checks that the docs do not list but that follow from them (our advice):

- **Validate again** with `omarchy plugin validate` and `qmllint`, from Phase 2, after your last edit.
- **Test from a fresh clone of your public repo**, not your working folder. This catches files you forgot to commit.
- **Test with the optional dependencies missing.** If your README names an optional tool, remove it and confirm the plugin degrades politely.

```bash
git clone https://github.com/yourname/custom-clock.git /tmp/custom-clock-check
omarchy plugin validate /tmp/custom-clock-check
```

*What just happened:* you ran the same manifest checks Omarchy runs at install time, against exactly what a user would receive. If it prints nothing and exits cleanly, the layout is sound.

## Turn the clone into a plugin

A clone is made to be temporary. Before you publish:

1. **Choose a permanent namespaced id.** The official example is `io.github.yourname.custom-clock`. Change it in `manifest.json` and in `moduleName` in every QML file that sets it. The official example uses the plugin id as `moduleName` in both files, so keep them in sync.
2. **Remove `omarchy.clonedFrom`.** It is development-only; its job was to restore the built-in when you removed the clone.
3. **Rewrite the description** to say what your plugin does, not what you cloned.
4. **Check the license.** The official example's LICENSE is MIT and lists the Omarchy copyright line (David Heinemeier Hansson) next to the plugin author's. If you started from a built-in, follow the same pattern and keep that notice.

Here is the finished manifest from the official guide, with the clone-only field gone:

```json
{
  "schemaVersion": 1,
  "id": "io.github.yourname.custom-clock",
  "name": "Custom Clock",
  "version": "1.0.0",
  "author": "Your name",
  "license": "MIT",
  "description": "A small clock with a details panel for the Omarchy bar.",
  "kinds": ["bar-widget"],
  "entryPoints": { "barWidget": "BarWidget.qml" },
  "barWidget": {
    "displayName": "Custom Clock",
    "category": "Time",
    "allowMultiple": false,
    "defaultSection": "center"
  }
}
```

The official guide adds a caution to take literally: use the example as a structural reference. Do not copy the id, repository URL, author, or description unchanged.

## Prepare the repository

The marketplace asks for four things, plus one optional:

- A **public GitHub repository**.
- A valid `manifest.json` **in the repository root**.
- A **README and a license**.
- **Safe install and removal**.
- Optionally a `preview.png`, which the marketplace optimizes for you.

The README the official guide shows is a good template, with four short sections: install, usage, configure, remove.

````markdown
# Custom Clock

A small clock with a details panel for the Omarchy Quattro bar.

## Install

```sh
omarchy plugin add https://github.com/yourname/custom-clock.git --enable
```

## Usage

Click the clock to open or close the details panel. Press Escape to close it.

## Configure

```sh
omarchy bar move io.github.yourname.custom-clock --section center
```

## Remove

```sh
omarchy plugin remove io.github.yourname.custom-clock
```
````

Add to it everything Phase 3 asked you to document: each external dependency, setup step, privilege boundary, service, installer, or remote build. If you ship a keybinding fragment or menu rows, say how to add and how to remove them.

Copy the files into a new working folder outside the plugins directory, so your repository is separate from the live clone, and make the id, `clonedFrom`, README, and LICENSE edits there. Put it under git (see [Git From Zero](/guides/git-from-zero) for what each command does), create the empty public repository on GitHub, and push:

```bash
mkdir -p ~/code/custom-clock
cp -r ~/.config/omarchy/plugins/yourname.clock/. ~/code/custom-clock/
cd ~/code/custom-clock
git init
git add .
git commit -m "Add custom clock plugin"
git branch -M main
git remote add origin https://github.com/yourname/custom-clock.git
git push -u origin main
```

> ⚠️ **Gotcha.** Set the permanent id in `manifest.json` before you commit, and read `git status` before the first commit. Everything you push is public, so never commit keys, tokens, or personal config.

Then install it the way a user would, from your own URL. Remove the clone first, so the two do not fight over the same widget:

```bash
omarchy plugin remove yourname.clock
omarchy plugin add https://github.com/yourname/custom-clock.git --enable
```

If that works, your plugin installs from nothing but your repo, under its permanent id.

## Submit to the marketplace

Publishing is three steps on [plugins.omarchy.org/publish.html](https://plugins.omarchy.org/publish.html):

1. **Prepare the repository** (above).
2. **Add a manifest** with every field the listing needs, and validate it. The required fields are `schemaVersion`, `id`, `name`, `version` (up to 64 characters), `author`, `description`, `kinds`, and `entryPoints`.
3. **Submit** with the [issue form](https://github.com/omacom/omarchy-plugin-marketplace/issues/new?template=submit-plugin.yml): your repository link, a category, and tags. Automated validation checks the current commit before a maintainer approves the listing.

The page says it plainly: the marketplace validates listings, not plugin security, and you remain responsible for your code, assets, documentation, and license.

Skipping the marketplace is also legitimate. A public git repo is the whole distribution mechanism: anyone can run `omarchy plugin add` against your URL.

## Shipping updates

When users run `omarchy plugin update <id>`, Omarchy fast-forwards their checkout to your latest commits, shows them the diff first, and rolls back if the new revision fails validation. Three habits follow from that:

- **Validate before every push.** A revision that fails validation does not reach users, but it also does not help them.
- **Do not rewrite published history.** An update is a fast-forward, and a rewritten branch cannot be fast-forwarded.
- **Make diffs reviewable.** Users are told to read the diff; small, focused commits make that review possible. Bump `version` when you release.

Put the checklist to work:

```exercise
[
  {
    "type": "task",
    "task": "Take the plugin you built or cloned and take it to publish-ready. Work through the list, then check each item off.",
    "reveal": "A publish-ready plugin has a permanent namespaced id (not omarchy.*) matching moduleName in every QML file, no omarchy.clonedFrom, a valid manifest in the repo root, a README with install/usage/configure/remove and every dependency documented, a license, and passes omarchy plugin validate from a fresh clone.",
    "checklist": ["Permanent namespaced id set in manifest and in every moduleName", "omarchy.clonedFrom removed", "README documents install, usage, configure, remove, and every dependency", "LICENSE present", "omarchy plugin validate passes on a fresh clone of the public repo", "Click, Escape, summon, hide, disable, enable, shell restart, and remove all tested"]
  }
]
```

Check yourself before moving on:

```quiz
[
  {
    "q": "Which change turns a development clone into a publishable plugin?",
    "choices": [
      "Rename the folder only",
      "Set a permanent namespaced id, remove omarchy.clonedFrom, and move the files into a public repo with README and license",
      "Add the omarchy. prefix to the id so it looks official"
    ],
    "answer": 1,
    "explain": "The clone id is temporary, clonedFrom is development-only, and the omarchy. prefix is reserved and rejected by the validator.",
    "why": ["The manifest id and every moduleName must change too.", null, "The reserved omarchy.* namespace is refused outright."]
  },
  {
    "q": "A user runs omarchy plugin update on your plugin. Which statement is true?",
    "choices": [
      "It reinstalls the plugin from scratch and discards their settings",
      "It fast-forwards their checkout to your newer commits, shows the diff first, and rolls back if the new revision fails validation",
      "It only works if the plugin is listed on the marketplace"
    ],
    "answer": 1,
    "explain": "Plugins are plain git checkouts; update is a fast-forward pull with a diff preview and a validation rollback."
  },
  {
    "q": "Your plugin is approved and listed on the marketplace. What does that mean for its security?",
    "choices": [
      "The maintainers audited it, so users do not need to read it",
      "Nothing about security: the marketplace validates listings, not plugin security, and you remain responsible for your code",
      "Omarchy sandboxes listed plugins"
    ],
    "answer": 1,
    "explain": "The publishing page states this directly. Document and bound what your plugin does."
  }
]
```

## Recap

1. Test the way a stranger will: click, Escape, summon and hide, disable and re-enable, shell restart, removal; also from a fresh clone of your public repo.
2. Replace the clone id with a permanent namespaced one (for example `io.github.yourname.custom-clock`), keep `moduleName` in sync, and delete `omarchy.clonedFrom`.
3. A listing needs a public GitHub repo, `manifest.json` at its root, a README and license, and safe install and removal; a `preview.png` is optional.
4. Submit through the marketplace issue form with the repository, a category, and tags; automated validation checks the commit, then a maintainer approves.
5. Users update by fast-forward with a diff preview and a validation rollback, so do not rewrite published history.
6. A public git repo alone is a complete distribution: anyone can `omarchy plugin add` your URL.

Your plugin is on its way. If an experiment ever leaves the desktop in a state you cannot get out of, [When Omarchy Breaks](/guides/when-omarchy-breaks) is the recovery guide.
