Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA 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.
Third-party menu plugins (e.g. omarchy.menu clones) get an empty Apps list: manifests passed through the panel Instantiator lose array types
Reported by ekollof · filed Sep 7, 2026 · open at source
This defect is tracked under R-5209. This report describes the same defect, so it is folded under it in the inbox.
Earliest report of this defect: R-860 (filed Sep 7, 2026).
Decided by agents · verified· Oct 9, 2026
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?
- 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?
- Decided by agents and verified by a second, independent agent — see the decision.
Original report
System details
AMD Ryzen AI 9 HX 370 (Radeon 890M), Omarchy dev (430e6791, tracking upstream/quattro), Quickshell 0.3.1-1, Hyprland (Wayland). omarchy-debug output can be attached on request.
What's wrong?
Any third-party menu plugin — including the supported clone of the built-in omarchy.menu — gets an empty Apps list ("Nothing here yet"). The app-list data itself is present and healthy; the plugin just never receives the application-library facade it is supposed to get.
Steps to reproduce
- Clone the built-in menu plugin so a third-party copy takes over the apps menu, e.g. copy
shell/plugins/menu/to~/.config/omarchy/plugins/ekollof.menu/and set"omarchy": { "clonedFrom": "omarchy.menu" }in its manifest.json (this is the layoutomarchy plugin cloneproduces). - Make sure the clone is the enabled menu in
~/.config/omarchy/shell.json(the registry'sresolveEnabledIdmapsomarchy.menuto the active clone viaclonedFrom). omarchy restart shell, then press Super+Alt+Space and open "Apps...".
Expected: the full application list. Actual: "Nothing here yet", every time, across restarts.
Root cause
The panel loader instantiates panel/menu plugins through a Quickshell Instantiator whose model is a JS array of entry objects that each carry the plugin manifest. On assignment the loader does:
// shell.qml, panel loader onLoaded
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)panelEntry.manifest here is modelData — it has crossed Qt's C++ model boundary, so the manifest object arrives as a QVariantMap (observable: Object.keys() order becomes alphabetical) and the nested kinds array is no longer a JS Array:
// capability check, shell.qml
function manifestHasKind(manifest, kind) {
return !!manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.indexOf(kind) !== -1
}Array.isArray(QVariantList) is false, so manifestHasKind(manifest, "menu") is false for every panel-delivered manifest, and createScopedPluginShell builds the scoped plugin shell API with appLibrary: null:
appLibrary: shell.manifestHasKind(manifest, "menu")
? shell.pluginAppLibraryFor(cacheKey, key) : null,The menu plugin's mergeAppRows() then sees root.shell.appLibrary == null and early-returns — the empty state.
First-party plugins are unaffected only by accident: pluginShellFor returns the shared host shell when manifest.__isFirstParty is truthy, and booleans survive the QVariant round-trip. Any third-party manifest with kind "menu" (clones, community menu plugins) silently loses the facade. pluginShellCapabilityProfile has the same exposure: it computes the cached API's profile via manifestHasKind, so the capability cache itself is poisoned by the QVariant-ified manifest — an earlier, correct API gets revoked and replaced by a no-menu-capability one about a second after startup, when the panel model delivers.
Instrumented evidence from a repro (same plugin, two delivery paths):
KIND-DEBUG manifest=ekollof.menu isArray=true idx=0 result=true // from registry (configureBar path)
KIND-DEBUG manifest=ekollof.menu isArray=false result=false // from panel Instantiator modelData
SHELL-DEBUG created api key=ekollof.menu appLibrary=present // early, correct
SHELL-DEBUG created api key=ekollof.menu appLibrary=NULL // after panel model deliversProposed fix
Capability decisions should run on the registry's raw manifest for the id, not on a copy that may have crossed a model boundary. PluginRegistry.installedPlugins always holds the original JS objects (kinds is a real array there):
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -725,7 +725,13 @@ ShellRoot {
if (!manifest || manifest.__isFirstParty) return shell
var key = String(manifest.id || "")
if (!key) return null
- return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
+ // A manifest handed through a QObject model (e.g. the panel
+ // Instantiator's modelData) arrives as a QVariantMap: nested arrays are
+ // no longer JS Arrays, so kind checks would under-declare capabilities.
+ // The registry always holds the raw manifest for this id; prefer it.
+ var raw = shell.pluginRegistry ? shell.pluginRegistry.installedPlugins[key] : null
+ var resolved = raw && raw.id === key ? raw : manifest
+ return shell.createScopedPluginShell(resolved, key, true, shell.pluginHasBarCapabilities(resolved))
}Verified on the affected machine: with this change the cloned menu's Apps submenu is fully populated after omarchy restart shell, and the debug logging above shows a single API creation with the facade intact.
Alternatives worth considering (maintainer's call)
- Make
manifestHasKind(andpluginShellCapabilityProfile) duck-typed instead of relying onArray.isArray, e.g. treat any object withlengthand index access as a list. Fragile, and leavespluginHasBarCapabilities/isAuthenticationServicewith the same trap. - Stop routing manifests through the panel model: carry only the plugin id (plus scalar fields) in the entry objects and have the delegate fetch
pluginRegistry.installedPlugins[id]directly. Cleanest, but a larger refactor ofcomputePanelEntriesand the delegate bindings.
The proposed one-place fix also keeps item.manifest assignments safe as-is, since those already go through publicPluginManifest's JSON round-trip, which restores real arrays.
View raw text
### System details
AMD Ryzen AI 9 HX 370 (Radeon 890M), Omarchy dev (`430e6791`, tracking `upstream/quattro`), Quickshell 0.3.1-1, Hyprland (Wayland). `omarchy-debug` output can be attached on request.
### What's wrong?
Any third-party menu plugin — including the supported clone of the built-in `omarchy.menu` — gets an empty Apps list ("Nothing here yet"). The app-list data itself is present and healthy; the plugin just never receives the application-library facade it is supposed to get.
#### Steps to reproduce
1. Clone the built-in menu plugin so a third-party copy takes over the apps menu, e.g. copy `shell/plugins/menu/` to `~/.config/omarchy/plugins/ekollof.menu/` and set `"omarchy": { "clonedFrom": "omarchy.menu" }` in its manifest.json (this is the layout `omarchy plugin clone` produces).
2. Make sure the clone is the enabled menu in `~/.config/omarchy/shell.json` (the registry's `resolveEnabledId` maps `omarchy.menu` to the active clone via `clonedFrom`).
3. `omarchy restart shell`, then press Super+Alt+Space and open "Apps...".
Expected: the full application list. Actual: "Nothing here yet", every time, across restarts.
#### Root cause
The panel loader instantiates panel/menu plugins through a Quickshell `Instantiator` whose model is a JS array of entry objects that each carry the plugin manifest. On assignment the loader does:
```qml
// shell.qml, panel loader onLoaded
if ("shell" in item) item.shell = shell.pluginShellFor(panelEntry.manifest)
```
`panelEntry.manifest` here is `modelData` — it has crossed Qt's C++ model boundary, so the manifest object arrives as a QVariantMap (observable: `Object.keys()` order becomes alphabetical) and the nested `kinds` array is no longer a JS Array:
```qml
// capability check, shell.qml
function manifestHasKind(manifest, kind) {
return !!manifest && Array.isArray(manifest.kinds)
&& manifest.kinds.indexOf(kind) !== -1
}
```
`Array.isArray(QVariantList)` is false, so `manifestHasKind(manifest, "menu")` is false for every panel-delivered manifest, and `createScopedPluginShell` builds the scoped plugin shell API with `appLibrary: null`:
```qml
appLibrary: shell.manifestHasKind(manifest, "menu")
? shell.pluginAppLibraryFor(cacheKey, key) : null,
```
The menu plugin's `mergeAppRows()` then sees `root.shell.appLibrary == null` and early-returns — the empty state.
First-party plugins are unaffected only by accident: `pluginShellFor` returns the shared host shell when `manifest.__isFirstParty` is truthy, and booleans survive the QVariant round-trip. Any third-party manifest with kind `"menu"` (clones, community menu plugins) silently loses the facade. `pluginShellCapabilityProfile` has the same exposure: it computes the cached API's profile via `manifestHasKind`, so the capability cache itself is poisoned by the QVariant-ified manifest — an earlier, correct API gets revoked and replaced by a no-menu-capability one about a second after startup, when the panel model delivers.
Instrumented evidence from a repro (same plugin, two delivery paths):
```
KIND-DEBUG manifest=ekollof.menu isArray=true idx=0 result=true // from registry (configureBar path)
KIND-DEBUG manifest=ekollof.menu isArray=false result=false // from panel Instantiator modelData
SHELL-DEBUG created api key=ekollof.menu appLibrary=present // early, correct
SHELL-DEBUG created api key=ekollof.menu appLibrary=NULL // after panel model delivers
```
#### Proposed fix
Capability decisions should run on the registry's raw manifest for the id, not on a copy that may have crossed a model boundary. `PluginRegistry.installedPlugins` always holds the original JS objects (`kinds` is a real array there):
```diff
--- a/shell/shell.qml
+++ b/shell/shell.qml
@@ -725,7 +725,13 @@ ShellRoot {
if (!manifest || manifest.__isFirstParty) return shell
var key = String(manifest.id || "")
if (!key) return null
- return shell.createScopedPluginShell(manifest, key, true, shell.pluginHasBarCapabilities(manifest))
+ // A manifest handed through a QObject model (e.g. the panel
+ // Instantiator's modelData) arrives as a QVariantMap: nested arrays are
+ // no longer JS Arrays, so kind checks would under-declare capabilities.
+ // The registry always holds the raw manifest for this id; prefer it.
+ var raw = shell.pluginRegistry ? shell.pluginRegistry.installedPlugins[key] : null
+ var resolved = raw && raw.id === key ? raw : manifest
+ return shell.createScopedPluginShell(resolved, key, true, shell.pluginHasBarCapabilities(resolved))
}
```
Verified on the affected machine: with this change the cloned menu's Apps submenu is fully populated after `omarchy restart shell`, and the debug logging above shows a single API creation with the facade intact.
#### Alternatives worth considering (maintainer's call)
- Make `manifestHasKind` (and `pluginShellCapabilityProfile`) duck-typed instead of relying on `Array.isArray`, e.g. treat any object with `length` and index access as a list. Fragile, and leaves `pluginHasBarCapabilities`/`isAuthenticationService` with the same trap.
- Stop routing manifests through the panel model: carry only the plugin id (plus scalar fields) in the entry objects and have the delegate fetch `pluginRegistry.installedPlugins[id]` directly. Cleanest, but a larger refactor of `computePanelEntries` and the delegate bindings.
The proposed one-place fix also keeps `item.manifest` assignments safe as-is, since those already go through `publicPluginManifest`'s JSON round-trip, which restores real arrays.
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
- Duplicate ofR-5209Cloned menu gets null appLibrary — manifestHasKind Array.isArray fails on QVariantList kinds· decided by agents · verified
- Fixed bypr/14767· confirmed
- Fixed bypr/15053· confirmed
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.