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-180GitHub issuePublicReceived

Cloned menu plugin loses its scoped shell API: Apps submenu permanently empty

Reported by Pierre-Aoki · filed Sep 23, 2026 · open at source

In the audited area (menu) — decided by agents · verified. Not validated 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?
AreaIn the audited areamenu· Decided by agents · verified
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

Cloned menu plugin loses its scoped shell API: Apps submenu permanently emptyrevision 1 · captured 2026-09-28 21:23 UTC via poll

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

{
  "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, 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.

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).

  1. Sep 23, 2026R-180 · this report— tracked here
  2. ·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.