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.
Cloned menu plugin loses its scoped shell API: Apps submenu permanently empty
Reported by Pierre-Aoki · filed Sep 23, 2026 · open at source
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?
- Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. So far: in the audited area (menu) — decided by agents · verified; matches no known issue — decided by arbitration. The outcome (valid, duplicate, not a bug, …) is not decided yet. See the decision.
Original report
System details
Intel Core Ultra 9 275HX, NVIDIA RTX 5090 Max-Q (nvidia 610.57.04) + Intel Arrow Lake-S (i915), Omarchy 4.0.3 (stable channel)
What's wrong?
What happens
A third-party plugin of kind menu is handed its scoped PluginShellApi on load, and that
object is destroyed immediately afterwards. The plugin's shell property goes back to null
and is never re-injected, because menu plugins are keepLoaded: true and the panel Loader
only assigns shell in onLoaded.
The visible consequence: **after omarchy plugin clone omarchy.menu, the cloned menu's Apps
submenu is permanently empty** ("0 entries"), and applications no longer appear when searching
the menu. Every other submenu still works — only the app rows are gone, becauseMenu.qml:mergeAppRows() starts with if (!root.appLibrary) return, and appLibrary is read
through root.shell.
The built-in omarchy.menu is unaffected: pluginShellFor() returns the real shell for a
first-party manifest (shell/shell.qml:740), so there is no revocable facade in that path.
Only a cloned/third-party menu goes through createScopedPluginShell().
Expected
A plugin of kind menu keeps a usable shell (and therefore shell.appLibrary) for as long
as it is loaded.
Steps to reproduce
omarchy plugin clone omarchy.menu(accept the default clone id)omarchy restart shell- Open the menu and enter Apps
The list is empty. Typing an application name in the menu search returns nothing either;
non-application entries still match.
Minimal reproduction
A 20-line third-party plugin of kind menu — no clone relationship, no bar widget, no other
entry point — shows the same lifecycle.
~/.config/omarchy/plugins/repro.menuprobe/manifest.json
{
"schemaVersion": 1,
"id": "repro.menuprobe",
"name": "Menu shell-token probe",
"version": "1.0.0",
"author": "repro",
"description": "Minimal third-party menu plugin that logs its injected shell",
"kinds": ["menu"],
"keepLoaded": true,
"entryPoints": { "menu": "Menu.qml" }
}~/.config/omarchy/plugins/repro.menuprobe/Menu.qml
import QtQuick
Item {
id: root
property var shell: null
property var manifest: null
onShellChanged: {
var lib = shell ? shell.appLibrary : null
var count = -1
try { if (lib) count = lib.sortedEntries("").length } catch (e) { count = -2 }
console.log("PROBE shell=" + shell + " appLibrary=" + lib + " entries=" + count)
}
function open(payloadJson) { return "ok" }
function close() { }
}Add {"id": "repro.menuprobe"} to plugins in ~/.config/omarchy/shell.json, thenomarchy restart shell. The log shows the facade arriving and leaving:
DEBUG qml: PROBE shell=PluginShellApi_QMLTYPE_60(0x7fae0103c300) appLibrary=null entries=-1
DEBUG qml: PROBE shell=null appLibrary=null entries=-1Note that shell.appLibrary is already null on the first line, while the facade itself is
still alive — consistent with revokePluginShellApi() destroying the app-library proxy and the
shell facade in that order, with the QML references dropping one event-loop pass apart.
The same trace, taken by instrumenting a cloned omarchy.menu (aoki.menu), reaches the dead
end explicitly:
DEBUG qml: DBG created root=Menu_QMLTYPE_442(0x7efd95d08ce0) shell=null
DEBUG qml: DBG shellChanged shell=PluginShellApi_QMLTYPE_29(0x7efd944befe0)
DEBUG qml: DBG shellChanged shell=null reg=PluginRegistryApi_QMLTYPE_19(0x7efd944bf520) regEnabled=true regManifest=aoki.menu
DEBUG qml: DBG loadProvider id=apps provider=apps itemCount=334
DEBUG qml: DBG startProvider id=apps entry=ok provider=apps
DEBUG qml: DBG mergeAppRows shell=NULL appLibrary=NULLregEnabled=true at that moment, so the plugin is not considered disabled when the facade
goes away.
Still present at v4.0.4
shell/shell.qml at tag v4.0.4 is byte-identical to the copy shipped in the 4.0.3 package
I am running, so nothing in 4.0.4 changes this path.
The capability-scoping work this comes from looks like 1702cf0b "Restrict third-party shell
plugin capabilities" (2026-09-01) and 203e1639 "Revoke stale plugin facade capabilities"
(2026-09-02). I cannot say from here whether the regression first shipped in 4.0.2 or 4.0.3 —
only that a menu clone made before that work stopped listing applications after updating.
Where this seems to live
I did not pin down which call performs the revocation, so these are pointers rather than a
diagnosis. revokePluginShellApi() (shell/shell.qml:559) is reached from two places:
createScopedPluginShell()(:577), when a cached facade'spluginId/profiledoes not
match what the caller asks for
- prunePluginApis() (:809), when !active || descriptor.profile !== expectedProfile
prunePluginApis() runs from syncPluginApis(), which is wired to bothpluginRegistry.onPluginsChanged and barWidgetRegistry.onChanged — so it fires several times
during startup and on every local plugin reload.
Whatever the trigger, the part that makes it permanent is that nothing re-injects: the panelLoader assigns shell only in onLoaded (:1342), and a keepLoaded plugin is not
reloaded afterwards. Bar widgets do recover, because Bar.qml:pluginBarApiFor() reassignsshell every time the bar rebuilds.
Scope
- Cloned menus: Apps submenu permanently empty. This is the user-visible bug.
- Any third-party
menu/panel/overlayplugin withkeepLoaded: truethat readsshell
loses it the same way; I only verified the menu kind.
- Bar widgets are unaffected.
Workaround
Re-enumerate .desktop entries in the plugin instead of going through shell.appLibrary,
reusing shell/services/hidden-entries.sh and default/omarchy/launcher.hides so the hidden
set stays identical to the host's.
Environment detail
- omarchy 4.0.3-1, omarchy-settings 4.0.3-1, stable channel
- quickshell 0.3.1-1, hyprland 0.56.2-2
- Kernel 7.2.3-arch1-3, SDDM, Hyprland session
- Two monitors (2560x1600 eDP-1, 1920x1080 HDMI-A-1)
Full omarchy-debug --no-sudo --print output available on request — it is mostly hardware
inventory, and the lines that matter are above.
View raw text
### System details
Intel Core Ultra 9 275HX, NVIDIA RTX 5090 Max-Q (nvidia 610.57.04) + Intel Arrow Lake-S (i915), Omarchy 4.0.3 (stable channel)
### What's wrong?
## What happens
A third-party plugin of kind `menu` is handed its scoped `PluginShellApi` on load, and that
object is destroyed immediately afterwards. The plugin's `shell` property goes back to `null`
and is never re-injected, because menu plugins are `keepLoaded: true` and the panel `Loader`
only assigns `shell` in `onLoaded`.
The visible consequence: **after `omarchy plugin clone omarchy.menu`, the cloned menu's Apps
submenu is permanently empty** ("0 entries"), and applications no longer appear when searching
the menu. Every other submenu still works — only the app rows are gone, because
`Menu.qml:mergeAppRows()` starts with `if (!root.appLibrary) return`, and `appLibrary` is read
through `root.shell`.
The built-in `omarchy.menu` is unaffected: `pluginShellFor()` returns the real `shell` for a
first-party manifest (`shell/shell.qml:740`), so there is no revocable facade in that path.
Only a cloned/third-party menu goes through `createScopedPluginShell()`.
## Expected
A plugin of kind `menu` keeps a usable `shell` (and therefore `shell.appLibrary`) for as long
as it is loaded.
## Steps to reproduce
1. `omarchy plugin clone omarchy.menu` (accept the default clone id)
2. `omarchy restart shell`
3. Open the menu and enter **Apps**
The list is empty. Typing an application name in the menu search returns nothing either;
non-application entries still match.
## Minimal reproduction
A 20-line third-party plugin of kind `menu` — no clone relationship, no bar widget, no other
entry point — shows the same lifecycle.
`~/.config/omarchy/plugins/repro.menuprobe/manifest.json`
```json
{
"schemaVersion": 1,
"id": "repro.menuprobe",
"name": "Menu shell-token probe",
"version": "1.0.0",
"author": "repro",
"description": "Minimal third-party menu plugin that logs its injected shell",
"kinds": ["menu"],
"keepLoaded": true,
"entryPoints": { "menu": "Menu.qml" }
}
```
`~/.config/omarchy/plugins/repro.menuprobe/Menu.qml`
```qml
import QtQuick
Item {
id: root
property var shell: null
property var manifest: null
onShellChanged: {
var lib = shell ? shell.appLibrary : null
var count = -1
try { if (lib) count = lib.sortedEntries("").length } catch (e) { count = -2 }
console.log("PROBE shell=" + shell + " appLibrary=" + lib + " entries=" + count)
}
function open(payloadJson) { return "ok" }
function close() { }
}
```
Add `{"id": "repro.menuprobe"}` to `plugins` in `~/.config/omarchy/shell.json`, then
`omarchy restart shell`. The log shows the facade arriving and leaving:
```
DEBUG qml: PROBE shell=PluginShellApi_QMLTYPE_60(0x7fae0103c300) appLibrary=null entries=-1
DEBUG qml: PROBE shell=null appLibrary=null entries=-1
```
Note that `shell.appLibrary` is already `null` on the first line, while the facade itself is
still alive — consistent with `revokePluginShellApi()` destroying the app-library proxy and the
shell facade in that order, with the QML references dropping one event-loop pass apart.
The same trace, taken by instrumenting a cloned `omarchy.menu` (`aoki.menu`), reaches the dead
end explicitly:
```
DEBUG qml: DBG created root=Menu_QMLTYPE_442(0x7efd95d08ce0) shell=null
DEBUG qml: DBG shellChanged shell=PluginShellApi_QMLTYPE_29(0x7efd944befe0)
DEBUG qml: DBG shellChanged shell=null reg=PluginRegistryApi_QMLTYPE_19(0x7efd944bf520) regEnabled=true regManifest=aoki.menu
DEBUG qml: DBG loadProvider id=apps provider=apps itemCount=334
DEBUG qml: DBG startProvider id=apps entry=ok provider=apps
DEBUG qml: DBG mergeAppRows shell=NULL appLibrary=NULL
```
`regEnabled=true` at that moment, so the plugin is not considered disabled when the facade
goes away.
## Still present at v4.0.4
`shell/shell.qml` at tag `v4.0.4` is byte-identical to the copy shipped in the 4.0.3 package
I am running, so nothing in 4.0.4 changes this path.
The capability-scoping work this comes from looks like `1702cf0b` "Restrict third-party shell
plugin capabilities" (2026-09-01) and `203e1639` "Revoke stale plugin facade capabilities"
(2026-09-02). I cannot say from here whether the regression first shipped in 4.0.2 or 4.0.3 —
only that a menu clone made before that work stopped listing applications after updating.
## Where this seems to live
I did not pin down which call performs the revocation, so these are pointers rather than a
diagnosis. `revokePluginShellApi()` (`shell/shell.qml:559`) is reached from two places:
- `createScopedPluginShell()` (`:577`), when a cached facade's `pluginId`/`profile` does not
match what the caller asks for
- `prunePluginApis()` (`:809`), when `!active || descriptor.profile !== expectedProfile`
`prunePluginApis()` runs from `syncPluginApis()`, which is wired to both
`pluginRegistry.onPluginsChanged` and `barWidgetRegistry.onChanged` — so it fires several times
during startup and on every local plugin reload.
Whatever the trigger, the part that makes it permanent is that nothing re-injects: the panel
`Loader` assigns `shell` only in `onLoaded` (`:1342`), and a `keepLoaded` plugin is not
reloaded afterwards. Bar widgets do recover, because `Bar.qml:pluginBarApiFor()` reassigns
`shell` every time the bar rebuilds.
## Scope
- Cloned menus: Apps submenu permanently empty. This is the user-visible bug.
- Any third-party `menu`/`panel`/`overlay` plugin with `keepLoaded: true` that reads `shell`
loses it the same way; I only verified the `menu` kind.
- Bar widgets are unaffected.
## Workaround
Re-enumerate `.desktop` entries in the plugin instead of going through `shell.appLibrary`,
reusing `shell/services/hidden-entries.sh` and `default/omarchy/launcher.hides` so the hidden
set stays identical to the host's.
## Environment detail
- omarchy 4.0.3-1, omarchy-settings 4.0.3-1, stable channel
- quickshell 0.3.1-1, hyprland 0.56.2-2
- Kernel 7.2.3-arch1-3, SDDM, Hyprland session
- Two monitors (2560x1600 eDP-1, 1920x1080 HDMI-A-1)
Full `omarchy-debug --no-sudo --print` output available on request — it is mostly hardware
inventory, and the lines that matter are above.
Also reported as
This defect is tracked under R-180 (filed Sep 23, 2026). 1 other report describes the same defect.
Earliest report of this defect: R-187 (filed Sep 22, 2026).
- Sep 23, 2026R-180 · this report— tracked here
- ·Decided by agents · verified
Open upstream.
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.
- Decided by agents · verified: in the audited area (menu)
- Decided by arbitration: matches no known issue
The outcome (valid, duplicate, not a bug, …) is not decided yet.