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

Plugin hot reload never clears the QML component cache: Qt.clearComponentCache is not a QML API, so the guard at shell.qml:757 is always false

Reported by johnstoj · filed Sep 2, 2026Closed upstream on 2026-09-20 by bjarneo via an automated message, as a duplicate of #6981 (R-1835). 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

Plugin hot reload never clears the QML component cache: Qt.clearComponentCache is not a QML API, so the guard at shell.qml:757 is always falserevision 1 · captured 2026-10-04 12:03 UTC via poll

What happened

A local plugin (omamail 0.7.0) was updated in place with git pull while the shell
was running. The shell logged its usual Local plugin changed, reloading: omamail,
but the plugin's window then refused to open — from the bar widget, from
omarchy-shell shell summon omamail, and from its .desktop entry. No window, no
error message, no crash (coredumpctl is empty). It stayed broken across further
reloads and was only fixed by restarting the quickshell process.

The quickshell log shows:

WARN scene: file:///home/john/.config/omarchy/plugins/omamail/App.qml[11:1]: Script file:///home/john/.config/omarchy/plugins/omamail/compose/Recovery.js unavailable
WARN scene: file:///home/john/.config/omarchy/plugins/omamail/compose/Recovery.js[-1:-1]: File name case mismatch

compose/Recovery.js exists, with exactly that case.

Why

shell/shell.qml, finishPluginReload() (line 757 as shipped in 4.0.2-1):

function finishPluginReload() {
  if (!shell.pluginReloading) return
  if (shell.pluginRegistry.scanning) {
    shell.pluginReloadPending = true
    return
  }
  if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache()
  shell.pluginRegistry.rescan()
}

Qt.clearComponentCache is not part of the QML Qt object. Clearing the cache is a
C++-only operation (QQmlEngine::clearComponentCache()); it has never been exposed to
QML. So typeof Qt.clearComponentCache === "function" is always false and the call
never runs — the reload path silently skips the one step it exists to perform.

Verified on this system (qt6-declarative 6.11.2-1):

typeof Qt.clearComponentCache -> "undefined"
typeof Qt.callLater           -> "function"   (control)

and the string clearComponentCache is not exported to QML by libQt6Qml.so.6 or by
quickshell.

Because the cache is never cleared, the engine keeps the type, script and
directory-resolution state it had before the plugin changed. In this case:

  • 07:33:05 — quickshell starts. omamail's Service.qml loads at startup and

imports compose/Senders.js, so the plugin's compose/ directory is resolved and
cached now. It holds only Senders.js. (Plugin service code is confirmed running at
07:33:42 by an unrelated warning from omamail/providers/AuthManager.qml.)
- 07:43:13 — git pull fast-forwards the plugin checkout and creates
compose/Recovery.js and compose/Recipients.js. Confirmed by btrfs birth times and
git reflog. Local plugin changed, reloading: omamail fires; the cache is not
cleared.
- 17:11:32 — first attempt to open the window. The panel is `"activation":
"on-demand", so App.qml` loads for the first time now. Its
import "compose/Recovery.js" (line 11) resolves against the stale 07:33 view of
that directory, where the file does not exist. Qt reports File name case mismatch,
the script import fails, the component never instantiates, and no window appears.

The file itself is fine: importing that exact path in a fresh Qt 6.11 engine
(/usr/lib/qt6/bin/qml) succeeds, while a control import of a genuinely missing file
in the same harness fails as expected. No symlinks anywhere in the path, no casefold
attribute on the btrfs directory, -rw-r--r--, valid ASCII JS, byte-exact filename.

I'm inferring the stale-directory-listing mechanism inside Qt from its behaviour rather
than from reading its source. What is directly established is that the dead guard at
line 757 means no cache is ever cleared, and that the same import succeeds in a fresh
engine and fails in the engine that was running before the file appeared.

Reproduce

  1. Install a local plugin with a service entry point that imports a .js file from a

subdirectory, and an on-demand panel entry point that imports a different .js
file from that same subdirectory.
2. Start the shell with only the first file present.
3. Add the second .js file to that directory (git pull in the plugin checkout, or
just copy it in). The shell logs Local plugin changed, reloading: <id>.
4. Summon the panel. It never opens; the log shows Script ... unavailable /
File name case mismatch.
5. omarchy restart shell — it works.

Expected

A plugin updated in place works after the shell's own hot reload, without the shell
needing a restart — which is what the Qt.clearComponentCache() line is there to
ensure.

Possible fix direction

The cache clear needs to happen somewhere it can actually be called. Options:

  • have quickshell expose an engine-level cache clear to QML and call that; or
  • reload the plugin through a path that recreates the engine.

Whatever the fix, the typeof ... === "function" guard should not be left to swallow a
name that never exists — if the capability is unavailable it should be loud, not a
silent skip. Note that the same defensive typeof idiom is used correctly here and
incorrectly omitted at lines 256 and 645, where an undefined errorString throws
instead (#9116).

This is not specific to one plugin: no plugin hot reload in 4.0.2-1 clears the
component cache, so any plugin that gains a file mid-session is affected.
compose/Recipients.js arrived in the same pull and would have failed identically once
compose opened.

System

  • Omarchy 4.0.2-1, stable channel (note: /usr/share/omarchy/version reads

4.0.0.alpha and appears stale)
- quickshell 0.3.1-1, qt6-declarative 6.11.2-1, hyprland 0.56.2-1
- Arch Linux, btrfs on /home
- Plugin: omamail 0.7.0
- omarchy debug is not a command in 4.0.2-1, so no debug log is attached.

Diagnosed by Claude Opus 5 via Claude Code.

View raw text
### What happened

A local plugin (omamail 0.7.0) was updated in place with `git pull` while the shell
was running. The shell logged its usual `Local plugin changed, reloading: omamail`,
but the plugin's window then refused to open — from the bar widget, from
`omarchy-shell shell summon omamail`, and from its `.desktop` entry. No window, no
error message, no crash (`coredumpctl` is empty). It stayed broken across further
reloads and was only fixed by restarting the quickshell process.

The quickshell log shows:

```
WARN scene: file:///home/john/.config/omarchy/plugins/omamail/App.qml[11:1]: Script file:///home/john/.config/omarchy/plugins/omamail/compose/Recovery.js unavailable
WARN scene: file:///home/john/.config/omarchy/plugins/omamail/compose/Recovery.js[-1:-1]: File name case mismatch
```

`compose/Recovery.js` exists, with exactly that case.

### Why

`shell/shell.qml`, `finishPluginReload()` (line 757 as shipped in 4.0.2-1):

```qml
function finishPluginReload() {
  if (!shell.pluginReloading) return
  if (shell.pluginRegistry.scanning) {
    shell.pluginReloadPending = true
    return
  }
  if (typeof Qt.clearComponentCache === "function") Qt.clearComponentCache()
  shell.pluginRegistry.rescan()
}
```

`Qt.clearComponentCache` is not part of the QML `Qt` object. Clearing the cache is a
C++-only operation (`QQmlEngine::clearComponentCache()`); it has never been exposed to
QML. So `typeof Qt.clearComponentCache === "function"` is always `false` and the call
never runs — the reload path silently skips the one step it exists to perform.

Verified on this system (qt6-declarative 6.11.2-1):

```
typeof Qt.clearComponentCache -> "undefined"
typeof Qt.callLater           -> "function"   (control)
```

and the string `clearComponentCache` is not exported to QML by `libQt6Qml.so.6` or by
quickshell.

Because the cache is never cleared, the engine keeps the type, script and
directory-resolution state it had before the plugin changed. In this case:

- **07:33:05** — quickshell starts. omamail's `Service.qml` loads at startup and
  imports `compose/Senders.js`, so the plugin's `compose/` directory is resolved and
  cached now. It holds only `Senders.js`. (Plugin service code is confirmed running at
  07:33:42 by an unrelated warning from `omamail/providers/AuthManager.qml`.)
- **07:43:13** — `git pull` fast-forwards the plugin checkout and creates
  `compose/Recovery.js` and `compose/Recipients.js`. Confirmed by btrfs birth times and
  `git reflog`. `Local plugin changed, reloading: omamail` fires; the cache is not
  cleared.
- **17:11:32** — first attempt to open the window. The panel is `"activation":
  "on-demand"`, so `App.qml` loads for the first time now. Its
  `import "compose/Recovery.js"` (line 11) resolves against the stale 07:33 view of
  that directory, where the file does not exist. Qt reports `File name case mismatch`,
  the script import fails, the component never instantiates, and no window appears.

The file itself is fine: importing that exact path in a fresh Qt 6.11 engine
(`/usr/lib/qt6/bin/qml`) succeeds, while a control import of a genuinely missing file
in the same harness fails as expected. No symlinks anywhere in the path, no `casefold`
attribute on the btrfs directory, `-rw-r--r--`, valid ASCII JS, byte-exact filename.

I'm inferring the stale-directory-listing mechanism inside Qt from its behaviour rather
than from reading its source. What is directly established is that the dead guard at
line 757 means no cache is ever cleared, and that the same import succeeds in a fresh
engine and fails in the engine that was running before the file appeared.

### Reproduce

1. Install a local plugin with a service entry point that imports a `.js` file from a
   subdirectory, and an `on-demand` panel entry point that imports a *different* `.js`
   file from that same subdirectory.
2. Start the shell with only the first file present.
3. Add the second `.js` file to that directory (`git pull` in the plugin checkout, or
   just copy it in). The shell logs `Local plugin changed, reloading: <id>`.
4. Summon the panel. It never opens; the log shows `Script ... unavailable` /
   `File name case mismatch`.
5. `omarchy restart shell` — it works.

### Expected

A plugin updated in place works after the shell's own hot reload, without the shell
needing a restart — which is what the `Qt.clearComponentCache()` line is there to
ensure.

### Possible fix direction

The cache clear needs to happen somewhere it can actually be called. Options:

- have quickshell expose an engine-level cache clear to QML and call that; or
- reload the plugin through a path that recreates the engine.

Whatever the fix, the `typeof ... === "function"` guard should not be left to swallow a
name that never exists — if the capability is unavailable it should be loud, not a
silent skip. Note that the same defensive `typeof` idiom is used correctly here and
*incorrectly* omitted at lines 256 and 645, where an undefined `errorString` throws
instead (#9116).

This is not specific to one plugin: no plugin hot reload in 4.0.2-1 clears the
component cache, so any plugin that gains a file mid-session is affected.
`compose/Recipients.js` arrived in the same pull and would have failed identically once
compose opened.

### System

- Omarchy 4.0.2-1, stable channel (note: `/usr/share/omarchy/version` reads
  `4.0.0.alpha` and appears stale)
- quickshell 0.3.1-1, qt6-declarative 6.11.2-1, hyprland 0.56.2-1
- Arch Linux, btrfs on `/home`
- Plugin: omamail 0.7.0
- `omarchy debug` is not a command in 4.0.2-1, so no debug log is attached.

Diagnosed by Claude Opus 5 via Claude Code.

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.