Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-5078GitHub issuePublicReceived

Third-party menu+bar-widget clone plugins can get appLibrary=null forever (Apps list permanently empty)

Reported by Thomster · filed Sep 14, 2026Closed upstream on 2026-09-17 by its own reporter Thomster. GitHub close reason: completed. Closed by its own reporter — not by itself evidence of a fix.

Received and recorded verbatim. Nothing has been checked yet.

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
Not yet mapped to any requirement or component.
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.

Original report

Third-party menu+bar-widget clone plugins can get appLibrary=null forever (Apps list permanently empty)revision 1 · captured 2026-10-04 12:10 UTC via poll

What happened

For a third-party "menu" plugin whose manifest declares "kinds": ["menu", "bar-widget"] (i.e. it's both a bar widget and a clone of omarchy.menu), the Apps submenu can come up permanently empty ("Nothing here yet"), and it never recovers — not after waiting, not after reopening the menu, only after restarting the shell (and even then it's a coin flip).

Expected

Apps should always populate once DesktopEntries finishes its (async, ~3-5s) initial scan, regardless of which plugin is providing the menu UI.

Root cause

Traced with instrumented debug builds (details below). Two compounding issues in shell.qml / services/:

1. Race in createScopedPluginShell() / manifestHasKind()

A plugin with both menu and bar-widget kinds gets instantiated twice near-simultaneously at startup (once via the bar-widget Loader, once via the panelEntries Instantiator for the menu kind). Both paths call shell.pluginShellFor(manifest) → createScopedPluginShell(manifest, ...) (shell.qml:569), which gates app-library access on:

function manifestHasKind(manifest, kind) {
  return !!manifest && Array.isArray(manifest.kinds)
    && manifest.kinds.indexOf(kind) !== -1
}

Logging Array.isArray(manifest.kinds) alongside JSON.stringify(manifest.kinds) at the appLibrary: shell.manifestHasKind(manifest, "menu") ? shell.pluginAppLibraryFor(...) : null call site (shell.qml:601-604) shows the two nearly-simultaneous calls for the same plugin sometimes disagree:

key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=true  indexOf=0 hasKindMenu=true
key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=false indexOf=0 hasKindMenu=false   <-- same textual kinds, Array.isArray fails
key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=true  indexOf=0 hasKindMenu=true

manifest.kinds prints identically and still supports .indexOf on the "false" run, but fails Array.isArray — looks like installedPlugins[key].kinds isn't consistently a real JS array across concurrent reads of PluginRegistry.installedPlugins (possibly a marshalling artifact when two call sites race to read/clone the same manifest during registry population). Whichever instantiation happens to land on isArray=false gets appLibrary: null baked into its PluginShellApi, and — because item.shell = shell.pluginShellFor(...) is only set once in the Loader.onLoaded handler (shell.qml:1342) — that null is permanent for the life of the shell process for that Loader, even though other callers later resolve the same plugin correctly (visible in the 3rd log line above, a different cache slot).

2. No working self-heal even when it should retry

PluginAppLibraryApi.qml declares:

signal appsChanged()

but nothing in shell.qml ever emits it (pluginAppLibraryFor(), shell.qml:423, wires _sortedEntries/_entryName/etc. through to the real AppLibrary, but never relays its appsChanged). Any third-party menu that listens for appLibrary.onAppsChanged (as the reference clone-plugin pattern encourages) to refresh its Apps list once DesktopEntries finishes its async scan gets nothing — so even a plugin that eagerly merges apps before the scan completes has no reliable way to pick up the rest as they arrive.

Steps to reproduce

  1. Clone omarchy.menu (omarchy plugin clone omarchy.menu or similar) and add "bar-widget" alongside "menu" in the clone's kinds, so it's both the bar's menu-launcher widget and a menu panel.
  2. Add it to shell.json's bar layout and disable the first-party omarchy.menu (disabledPlugins: ["omarchy.menu"]).
  3. Restart the shell repeatedly (omarchy restart shell) and open Apps each time. It comes up empty on a meaningful fraction of restarts, and never self-heals once it does.

System

  • Omarchy 4.0.3-1
  • Hyprland 0.56.2, Quickshell 0.3.1-1, qt6-declarative 6.11.2-1
  • Arch Linux, kernel 7.2.3-arch1-3

Notes

  • Not reproducible with the first-party omarchy.menu — first-party manifests skip createScopedPluginShell entirely (pluginShellFor returns the raw shell singleton for manifest.__isFirstParty), so they never hit this path.
  • Screenshot of the empty Apps panel attached separately.
  • Happy to share the exact debug QML diffs used to capture the race if useful.
View raw text
## What happened

For a third-party "menu" plugin whose manifest declares `"kinds": ["menu", "bar-widget"]` (i.e. it's both a bar widget *and* a clone of `omarchy.menu`), the Apps submenu can come up permanently empty ("Nothing here yet"), and it never recovers — not after waiting, not after reopening the menu, only after restarting the shell (and even then it's a coin flip).

## Expected

Apps should always populate once `DesktopEntries` finishes its (async, ~3-5s) initial scan, regardless of which plugin is providing the menu UI.

## Root cause

Traced with instrumented debug builds (details below). Two compounding issues in `shell.qml` / `services/`:

### 1. Race in `createScopedPluginShell()` / `manifestHasKind()`

A plugin with both `menu` and `bar-widget` kinds gets instantiated twice near-simultaneously at startup (once via the bar-widget `Loader`, once via the `panelEntries` `Instantiator` for the `menu` kind). Both paths call `shell.pluginShellFor(manifest)` → `createScopedPluginShell(manifest, ...)` (shell.qml:569), which gates app-library access on:

```qml
function manifestHasKind(manifest, kind) {
  return !!manifest && Array.isArray(manifest.kinds)
    && manifest.kinds.indexOf(kind) !== -1
}
```

Logging `Array.isArray(manifest.kinds)` alongside `JSON.stringify(manifest.kinds)` at the `appLibrary: shell.manifestHasKind(manifest, "menu") ? shell.pluginAppLibraryFor(...) : null` call site (shell.qml:601-604) shows the two nearly-simultaneous calls for the *same plugin* sometimes disagree:

```
key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=true  indexOf=0 hasKindMenu=true
key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=false indexOf=0 hasKindMenu=false   <-- same textual kinds, Array.isArray fails
key=omarchy_plus_recents.menu ... kinds=["menu","bar-widget"] isArray=true  indexOf=0 hasKindMenu=true
```

`manifest.kinds` prints identically and still supports `.indexOf` on the "false" run, but fails `Array.isArray` — looks like `installedPlugins[key].kinds` isn't consistently a real JS array across concurrent reads of `PluginRegistry.installedPlugins` (possibly a marshalling artifact when two call sites race to read/clone the same manifest during registry population). Whichever instantiation happens to land on `isArray=false` gets `appLibrary: null` baked into its `PluginShellApi`, and — because `item.shell = shell.pluginShellFor(...)` is only set once in the `Loader.onLoaded` handler (shell.qml:1342) — that `null` is permanent for the life of the shell process for that Loader, even though *other* callers later resolve the same plugin correctly (visible in the 3rd log line above, a different cache slot).

### 2. No working self-heal even when it should retry

`PluginAppLibraryApi.qml` declares:

```qml
signal appsChanged()
```

but nothing in `shell.qml` ever emits it (`pluginAppLibraryFor()`, shell.qml:423, wires `_sortedEntries`/`_entryName`/etc. through to the real `AppLibrary`, but never relays its `appsChanged`). Any third-party menu that listens for `appLibrary.onAppsChanged` (as the reference clone-plugin pattern encourages) to refresh its Apps list once `DesktopEntries` finishes its async scan gets nothing — so even a plugin that eagerly merges apps before the scan completes has no reliable way to pick up the rest as they arrive.

## Steps to reproduce

1. Clone `omarchy.menu` (`omarchy plugin clone omarchy.menu` or similar) and add `"bar-widget"` alongside `"menu"` in the clone's `kinds`, so it's both the bar's menu-launcher widget and a `menu` panel.
2. Add it to `shell.json`'s bar layout and disable the first-party `omarchy.menu` (`disabledPlugins: ["omarchy.menu"]`).
3. Restart the shell repeatedly (`omarchy restart shell`) and open Apps each time. It comes up empty on a meaningful fraction of restarts, and never self-heals once it does.

## System

- Omarchy 4.0.3-1
- Hyprland 0.56.2, Quickshell 0.3.1-1, qt6-declarative 6.11.2-1
- Arch Linux, kernel 7.2.3-arch1-3

## Notes

- Not reproducible with the first-party `omarchy.menu` — first-party manifests skip `createScopedPluginShell` entirely (`pluginShellFor` returns the raw `shell` singleton for `manifest.__isFirstParty`), so they never hit this path.
- Screenshot of the empty Apps panel attached separately.
- Happy to share the exact debug QML diffs used to capture the race if useful.

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Not linked to any known issue, other report or change yet.

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

No decision has been recorded yet.