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.
4.0.3: third-party plugins cannot read their own shell.json entry, and lock clones cannot reach their own service
Reported by pata27 · filed Sep 9, 2026Closed upstream on 2026-09-21 by bjarneo via an automated message, as a duplicate of #10907 (R-796). GitHub close reason: completed.
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
Summary
Omarchy 4.0.3 leaves third-party non-bar plugins with no way to read their own configuration,
and leaves clones of omarchy.lock or omarchy.polkit with no way to reach their own service.
Both are silent: nothing is logged, and affected plugins simply come up with every setting at
its default and a UI whose buttons call nothing.
Found while debugging io.github.sirjul1337.lock-explorer after upgrading 4.0.2-1 -> 4.0.3-1.
Tested on omarchy 4.0.3-1, quickshell 0.3.1-1.
1. No read path for a third-party plugin's own entry settings
README.md states the contract:
Settings are inline on the entry. No config: sub-object, no separate per-plugin settings file, no merge layers. The fields on each entry are the values the plugin sees.For a third-party bar widget that holds. For a third-party service, overlay, panel ormenu it no longer does.
Non-first-party plugins now receive a scoped facade rather than the host:
// shell.qml:929
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)PluginShellApi exposes appLibrary, bar, barConfig, idleConfig, pluginId, and
lifecycle helpers. It has no shellConfig, and nothing else injects the entry either: the
service and panel loaders set omarchyPath, shell, manifest, barWidgetRegistry,pluginRegistry and service, but never the plugin's own plugins[] entry.
So a plugin whose entry is
{ "id": "io.github.sirjul1337.lock-explorer", "design": "my-terminal", "boot": "terminal", "unlock": "rise" }sees none of those values. First-party plugins are unaffected because pluginShellFor()
returns the real shell for them, and plugins/services/idle/Service.qml still readsshell.shellConfig.idle happily.
Writing still works, since updateEntryInline survives on the facade. That asymmetry is what
makes it destructive in practice: a plugin that does read-modify-write on its entry now reads{}, and the write replaces the entry with only the field it just set. Downstream report with
the data-loss detail: SirJul1337/omarchy-lock-explorer#19.
Reproduce:
omarchy-shell shell listShellConfig | jq '.plugins' # design: my-terminal
omarchy-shell lock status | jq '.design' # card2. A plugin cannot reach its own service if that service is an authentication service
The panel/overlay loader resolves the service through the public map:
// shell.qml:1349
if ("service" in item) item.service = shell.serviceFor(panelEntry.pluginId)
// shell.qml:886
return _services[String(pluginId)] || nullAuthentication services are intentionally kept out of that map:
// shell.qml:934-950
if (authenticationService) {
AuthServiceStore.put(key, inst)
} else {
_services = snext
}
...
return authenticationService ? null : (_services[key] || null)And a third-party clone inherits the capability from its source:
// services/PluginRegistry.qml:107-115
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
? source.__hostCapabilities.slice() : []omarchy.lock declares authentication, so every clone of it does too. The result is that a
lock-replacement plugin that pairs a service with an overlay gets service === null in its own
overlay, permanently. shell.serviceFor() on the facade routes to the same _services lookup,
so there is no alternative path.
The isolation goal reads as keeping other plugins and host traversal away from
credential-bearing QML. Handing a plugin its own service does not appear to serve that goal,
but it is what currently breaks.
Impact
Any third-party lock screen is affected. Symptoms reported by users: the picker opens butEnter selects nothing, the active entry always shows the built-in default, and the configured
design is ignored at lock time. See SirJul1337/omarchy-lock-explorer#15, #16, #19, #22.
Suggested directions
- Give third-party non-bar plugins a documented read path for their own entry, mirroring
updateEntryInline. Injecting the entry onto the instance, or an entrySettings property on
PluginShellApi, would both satisfy the README contract.
2. In the service injection at shell.qml:1349, resolve a plugin's own authentication service
from AuthServiceStore when pluginOwnsTarget(pluginId, pluginId) holds, so a plugin keeps
access to itself without widening anything for others.
3. Log when a plugin capability is withheld. Both failures here were silent, and the plugin had
no way to detect that a property it relied on had turned into undefined.
Happy to test patches on this machine.
View raw text
## Summary
Omarchy 4.0.3 leaves third-party non-bar plugins with no way to read their own configuration,
and leaves clones of `omarchy.lock` or `omarchy.polkit` with no way to reach their own service.
Both are silent: nothing is logged, and affected plugins simply come up with every setting at
its default and a UI whose buttons call nothing.
Found while debugging `io.github.sirjul1337.lock-explorer` after upgrading 4.0.2-1 -> 4.0.3-1.
Tested on omarchy 4.0.3-1, quickshell 0.3.1-1.
## 1. No read path for a third-party plugin's own entry settings
`README.md` states the contract:
> **Settings are inline on the entry.** No `config:` sub-object, no separate per-plugin
> settings file, no merge layers. The fields on each entry are the values the plugin sees.
For a third-party bar widget that holds. For a third-party `service`, `overlay`, `panel` or
`menu` it no longer does.
Non-first-party plugins now receive a scoped facade rather than the host:
```qml
// shell.qml:929
if ("shell" in inst) inst.shell = shell.pluginShellFor(manifest)
```
`PluginShellApi` exposes `appLibrary`, `bar`, `barConfig`, `idleConfig`, `pluginId`, and
lifecycle helpers. It has no `shellConfig`, and nothing else injects the entry either: the
service and panel loaders set `omarchyPath`, `shell`, `manifest`, `barWidgetRegistry`,
`pluginRegistry` and `service`, but never the plugin's own `plugins[]` entry.
So a plugin whose entry is
```json
{ "id": "io.github.sirjul1337.lock-explorer", "design": "my-terminal", "boot": "terminal", "unlock": "rise" }
```
sees none of those values. First-party plugins are unaffected because `pluginShellFor()`
returns the real `shell` for them, and `plugins/services/idle/Service.qml` still reads
`shell.shellConfig.idle` happily.
Writing still works, since `updateEntryInline` survives on the facade. That asymmetry is what
makes it destructive in practice: a plugin that does read-modify-write on its entry now reads
`{}`, and the write replaces the entry with only the field it just set. Downstream report with
the data-loss detail: SirJul1337/omarchy-lock-explorer#19.
Reproduce:
```bash
omarchy-shell shell listShellConfig | jq '.plugins' # design: my-terminal
omarchy-shell lock status | jq '.design' # card
```
## 2. A plugin cannot reach its own service if that service is an authentication service
The panel/overlay loader resolves the service through the public map:
```qml
// shell.qml:1349
if ("service" in item) item.service = shell.serviceFor(panelEntry.pluginId)
// shell.qml:886
return _services[String(pluginId)] || null
```
Authentication services are intentionally kept out of that map:
```qml
// shell.qml:934-950
if (authenticationService) {
AuthServiceStore.put(key, inst)
} else {
_services = snext
}
...
return authenticationService ? null : (_services[key] || null)
```
And a third-party clone inherits the capability from its source:
```qml
// services/PluginRegistry.qml:107-115
manifest.__hostCapabilities = source && Array.isArray(source.__hostCapabilities)
? source.__hostCapabilities.slice() : []
```
`omarchy.lock` declares `authentication`, so every clone of it does too. The result is that a
lock-replacement plugin that pairs a service with an overlay gets `service === null` in its own
overlay, permanently. `shell.serviceFor()` on the facade routes to the same `_services` lookup,
so there is no alternative path.
The isolation goal reads as keeping *other* plugins and host traversal away from
credential-bearing QML. Handing a plugin its own service does not appear to serve that goal,
but it is what currently breaks.
## Impact
Any third-party lock screen is affected. Symptoms reported by users: the picker opens but
`Enter` selects nothing, the active entry always shows the built-in default, and the configured
design is ignored at lock time. See SirJul1337/omarchy-lock-explorer#15, #16, #19, #22.
## Suggested directions
1. Give third-party non-bar plugins a documented read path for their own entry, mirroring
`updateEntryInline`. Injecting the entry onto the instance, or an `entrySettings` property on
`PluginShellApi`, would both satisfy the README contract.
2. In the service injection at `shell.qml:1349`, resolve a plugin's own authentication service
from `AuthServiceStore` when `pluginOwnsTarget(pluginId, pluginId)` holds, so a plugin keeps
access to itself without widening anything for others.
3. Log when a plugin capability is withheld. Both failures here were silent, and the plugin had
no way to detect that a property it relied on had turned into `undefined`.
Happy to test patches on this machine.
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.