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-179GitHub issuePublicDecided· Out of scope

Truncated shell.json falls back to builtin defaults; the next config write persists them (bar resets, third-party plugins disabled)

Reported by alanwcurry-dev · filed Sep 23, 2026 · open at source

Decided: Out of scope — decided by agents · verified 3 days ago.

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?
AreaOutside the audited area· 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?
Decided by agents and verified by a second, independent agent — see the decision.

Original report

Truncated shell.json falls back to builtin defaults; the next config write persists them (bar resets, third-party plugins disabled)revision 1 · captured 2026-09-28 21:23 UTC via poll

What happens

When ~/.config/omarchy/shell.json becomes empty (0 bytes) while the shell is running, the bar reverts to the stock layout and every third-party plugin is disabled — permanently, because the very next shell-side config write persists the shipped defaults over the user's file. No backup is written and nothing is logged.

This is the same mutateShellConfig fallback as #11349, but with a different trigger: #11349 is the startup window (a mutation landing before the async FileView load completes). This one is a runtime truncation — the user's file is already loaded and fine, then it becomes empty, and the shell turns that into a permanent reset.

Why

The truncation reaches the fallback through the FileView change/reload path:

// shell.qml
FileView {
    id: userConfigFile
    path: shell.userConfigPath
    watchChanges: true
    atomicWrites: true
    onLoaded: shell.applyShellConfig()
    onLoadFailed: function(error) { shell.applyShellConfig() }
    onFileChanged: reload()          // line 142
}

When the file changes (including becoming 0 bytes), reload() → onLoaded → applyShellConfig():

// shell.qml:73-88
function applyShellConfig() {
    var defaults = Util.isPlainObject(defaultsConfig) ? defaultsConfig : builtinShellConfig
    var user = null
    var userText = userConfigFile.text() || ""
    if (userText.trim()) {
        // parse; set user
    }
    shellConfig = user || defaults      // empty file → user null → builtinShellConfig (stock)
}

shellConfig is now the builtin stock config. The next plugin-initiated write — idle service, a widget persisting a setting, OmaSettings, omarchy bar set … — goes through mutateShellConfig():

// shell.qml:162-166  (also 1069 in updateEntryInline)
function mutateShellConfig(mutator) {
    var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
    mutator(copy)
    persistShellConfig(copy)      // writes the whole file — stock layout + plugins: []
}

…which writes the stock config back to disk. A transient truncation is now a permanent reset, and because enablement is stored in shell.json, every third-party plugin drops off the bar at once.

Evidence

On an edge-channel install I've seen this ~14 times across a week. A path-unit guard I run snapshots and restores the file, so the timeline is recorded:

  • 14 × shell.json.bak.EMPTY.* artifacts — the 0-byte truncations (Sep 16–19).
  • Guard log: after each truncation the file came back within ~2 s as a ~1370-byte config — the shipped default, not the user's (2312–2351-byte) config:
  2026-09-21 21:04:50 was mid-write (1372 bytes) — snapshot updated
  2026-09-22 20:37:01 was mid-write (1374 bytes) — snapshot updated
  • After restoring the user's config on Sep 21 22:55, the file was rewritten to stock again at 22:57 (shell.json.bak.RESET.20260921225710, 1374 bytes) — a settings change landing after a truncation, persisting the defaults.

The spontaneous 0-byte truncation itself is a separate, still-unexplained bug (every occurrence lands within the minute of a shell config reload, often right after an omarchy update; the shell's own FileView.setText with atomicWrites: true is the only non-atomic writer involved). But even if that trigger is never fixed, the shell should not amplify it into a permanent reset.

Reproduce

On a machine with a non-default layout (a third-party plugin in the bar, anything in plugins[]):

  1. While the shell is running: : > ~/.config/omarchy/shell.json
  2. Trigger any shell-side config write, e.g. omarchy bar set omarchy.clock format "dddd HH:mm" (or wait for the idle service / a widget to persist a setting).
  3. ~/.config/omarchy/shell.json is now byte-for-byte the shipped default plus that one change; the user's layout and every third-party plugin are gone, with no backup and no log line.

Suggested fixes

Any one of these would stop the amplifier (independent of, and complementary to, #11349):

  • In applyShellConfig, when the user file is empty/unparseable, keep the last successfully loaded user config (like #7723 proposes for parse failures) instead of falling back to builtinShellConfig — and never persist that fallback.
  • In mutateShellConfig / updateEntryInline, refuse to persist a config that is byte-identical to the shipped defaults when the last-ingested user config was not; log it.
  • Treat a mid-session empty/truncated file as "don't reload into defaults" — retain the in-memory shellConfig until a non-empty valid file arrives.

System

  • Omarchy 4.0.0.r2186.gee8ebf6-1 (edge), quickshell 0.3.1, Hyprland 0.56.2, Arch
  • ASUS ExpertBook PM3406CKA

Related: #11349 (same fallback, startup-window trigger), #7723 (keep-last-good on parse failure), #10885 (mutateShellConfig whole-file rewrite hazard).

View raw text
### What happens

When `~/.config/omarchy/shell.json` becomes empty (0 bytes) while the shell is running, the bar reverts to the stock layout and every third-party plugin is disabled — **permanently**, because the very next shell-side config write persists the shipped defaults over the user's file. No backup is written and nothing is logged.

This is the same `mutateShellConfig` fallback as #11349, but with a different trigger: #11349 is the startup window (a mutation landing before the async `FileView` load completes). This one is a **runtime truncation** — the user's file is already loaded and fine, then it becomes empty, and the shell turns that into a permanent reset.

### Why

The truncation reaches the fallback through the `FileView` change/reload path:

```qml
// shell.qml
FileView {
    id: userConfigFile
    path: shell.userConfigPath
    watchChanges: true
    atomicWrites: true
    onLoaded: shell.applyShellConfig()
    onLoadFailed: function(error) { shell.applyShellConfig() }
    onFileChanged: reload()          // line 142
}
```

When the file changes (including becoming 0 bytes), `reload()` → `onLoaded` → `applyShellConfig()`:

```qml
// shell.qml:73-88
function applyShellConfig() {
    var defaults = Util.isPlainObject(defaultsConfig) ? defaultsConfig : builtinShellConfig
    var user = null
    var userText = userConfigFile.text() || ""
    if (userText.trim()) {
        // parse; set user
    }
    shellConfig = user || defaults      // empty file → user null → builtinShellConfig (stock)
}
```

`shellConfig` is now the **builtin stock config**. The next plugin-initiated write — idle service, a widget persisting a setting, OmaSettings, `omarchy bar set …` — goes through `mutateShellConfig()`:

```qml
// shell.qml:162-166  (also 1069 in updateEntryInline)
function mutateShellConfig(mutator) {
    var copy = JSON.parse(JSON.stringify(shellConfig || builtinShellConfig))
    mutator(copy)
    persistShellConfig(copy)      // writes the whole file — stock layout + plugins: []
}
```

…which writes the stock config back to disk. A transient truncation is now a permanent reset, and because enablement is stored in shell.json, every third-party plugin drops off the bar at once.

### Evidence

On an edge-channel install I've seen this ~14 times across a week. A path-unit guard I run snapshots and restores the file, so the timeline is recorded:

- 14 × `shell.json.bak.EMPTY.*` artifacts — the 0-byte truncations (Sep 16–19).
- Guard log: after each truncation the file came back within ~2 s as a ~1370-byte config — the shipped default, not the user's (2312–2351-byte) config:

  ```
  2026-09-21 21:04:50 was mid-write (1372 bytes) — snapshot updated
  2026-09-22 20:37:01 was mid-write (1374 bytes) — snapshot updated
  ```

- After restoring the user's config on Sep 21 22:55, the file was rewritten to stock again at 22:57 (`shell.json.bak.RESET.20260921225710`, 1374 bytes) — a settings change landing after a truncation, persisting the defaults.

The spontaneous 0-byte truncation itself is a separate, still-unexplained bug (every occurrence lands within the minute of a shell config reload, often right after an `omarchy update`; the shell's own `FileView.setText` with `atomicWrites: true` is the only non-atomic writer involved). But even if that trigger is never fixed, the shell should not *amplify* it into a permanent reset.

### Reproduce

On a machine with a non-default layout (a third-party plugin in the bar, anything in `plugins[]`):

1. While the shell is running: `: > ~/.config/omarchy/shell.json`
2. Trigger any shell-side config write, e.g. `omarchy bar set omarchy.clock format "dddd HH:mm"` (or wait for the idle service / a widget to persist a setting).
3. `~/.config/omarchy/shell.json` is now byte-for-byte the shipped default plus that one change; the user's layout and every third-party plugin are gone, with no backup and no log line.

### Suggested fixes

Any one of these would stop the amplifier (independent of, and complementary to, #11349):

- In `applyShellConfig`, when the user file is empty/unparseable, keep the **last successfully loaded user config** (like #7723 proposes for parse failures) instead of falling back to `builtinShellConfig` — and never persist that fallback.
- In `mutateShellConfig` / `updateEntryInline`, refuse to persist a config that is byte-identical to the shipped defaults when the last-ingested user config was not; log it.
- Treat a mid-session empty/truncated file as "don't reload into defaults" — retain the in-memory `shellConfig` until a non-empty valid file arrives.

### System

- Omarchy 4.0.0.r2186.gee8ebf6-1 (edge), quickshell 0.3.1, Hyprland 0.56.2, Arch
- ASUS ExpertBook PM3406CKA

Related: #11349 (same fallback, startup-window trigger), #7723 (keep-last-good on parse failure), #10885 (`mutateShellConfig` whole-file rewrite hazard).

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.

  • Out of scopeDecided by agents · verified · 2026-10-09 17:32 UTC