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

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.

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

4.0.3: third-party plugins cannot read their own shell.json entry, and lock clones cannot reach their own servicerevision 1 · captured 2026-10-04 12:10 UTC via poll

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:

// 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 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:

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:

// 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:

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

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.