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-29GitHub issuePublicFixed

Menu JSONC: a top-level array root is silently read as menu rows with ids "0", "1", …

Reported by buger · filed Sep 28, 2026 · open at source · 5 revisions

Decided: Duplicate — decided by agents · verified 8 days ago.

At a glance

1 · What is claimed?
  • ConfirmedViolates SW-REQ-260922-3T3F: a top-level JSON array root renders its elements as menu rows keyed '0', '1', ... instead of yielding no items
  • ConfirmedReproducible: parseMenuJsonc('[{...},{...}]') returns two rows with ids '0' and '1'
  • ConfirmedAffects the menu component
  • LinkedMatches known issue KI-MENU-JSONC-ARRAY-ROOT
2 · What does it affect?
AreaIn the audited areamenu· Decided by agents · verified
3 · Is it reproducible?
An engine-executed run exited 1 and showed the defect signal. See reproducer
4 · What remains unclear?
  • Open question for maintainers: Reject an array root silently (empty contribution, the #13968 and #13681 behaviour, documented in docs/menu.md) or surface it as a config error?
5 · What happens next?
Decided by agents and verified by a second, independent agent — see the decision.

Original report

Menu JSONC: a top-level array root is silently read as menu rows with ids "0", "1", …revision 4 · captured 2026-10-02 09:39 UTC via poll

A hand-edited menu.jsonc whose root is a JSON array instead of an object is silently turned into menu rows with ids "0", "1", … parented to root. That includes the user extension ~/.config/omarchy/extensions/omarchy-menu.jsonc, which goes through the same parseMenuJsonc. Label-only elements stay hidden as empty submenus; only elements with an action show, under the user's own labels.

Reproduced on quattro (3faafba2, 2026-09-27), using the repo's own parser, stock file fetched straight from this repo, and then on the #13968 head b2dae8db (narrated, 43s):

https://github.com/user-attachments/assets/5075e08a-2ef3-4855-b229-eeefcd7567e9

Repro (run from the repo root, node ≥ 18):

// repro.js
const m = require("./shell/plugins/menu/MenuModel.js");

const rows = m.parseMenuJsonc('[{"label":"should-not-appear"},{"label":"ghost-2"}]');
console.log(JSON.stringify(rows));
// expected: []   actual: two rows with id "0" and "1", parent "root"

// every other non-object root behaves correctly:
for (const s of ["{}", "null", "42", '"str"']) console.log(s, m.parseMenuJsonc(s));
// -> all []

What happens. parseMenuJsonc guards the parsed root with

if (typeof parsed !== "object" || parsed === null) return [];

but typeof [] === "object", so an array root passes the guard. The for..in loop then iterates array indices as entry ids, and each object element becomes a row with id "0", "1", … — no error anywhere.

Blast radius. Anyone who hand-edits menu.jsonc and writes the root as a list of entries (a plausible mistake — it's the natural shape for "a list of menu items") gets rows keyed by array index with zero feedback about what went wrong: label-only entries are hidden, and entries with an action show under their own labels. Silent wrong behavior instead of either a clean empty menu or a config error.

Fix. One-line guard: reject Array.isArray(parsed) alongside the existing checks, matching what every other non-object root already does. That fix went out as #13511 and now lives in #13968, which replaces it and also fixes #13250 and #13493. #13968 makes both cases below give no rows, and it notes in docs/menu.md that a file whose root is not an object contributes no entries (as #13681 does), so a user whose entries vanish can see why.

What an array root shows on quattro 8b4eae6 (parsed ids, then visible labels)
node -e 'const fs=require("fs"), m=require("./shell/plugins/menu/MenuModel.js"); const stock=m.parseMenuJsonc(fs.readFileSync("default/omarchy/omarchy-menu.jsonc","utf8")); for (const u of [`[{"label":"x"},{"label":"y"}]`, `[{"label":"Notes","action":"echo notes"}]`]) { const p=m.parseMenuJsonc(u), {items,itemOrder}=m.mergeMenuSources(stock,p); console.log(p.map(r=>r.id), p.filter(r=>m.isVisible(items,itemOrder,{},items[r.id])).map(r=>items[r.id].label)) }'
[ '0', '1' ] []
[ '0' ] [ 'Notes' ]

A 19-second capture of the shipped parser on quattro 8b4eae6 and then with the fix in #13968 (b2dae8db), reading a menu file whose top level is an array: quattro invents rows 0 and 1, the fix returns 0 rows, as it already does for other non-object roots:

https://github.com/user-attachments/assets/6781bd5d-d2d9-46ee-ad85-1f08ffc2f1cd

Independent verification: our validation of this report (reproducer and verdict) · the known-issue record (status and tests).


Updated 2026-10-01: corrected an overstatement — label-only array elements stay hidden and only elements with an action show, under the user's own labels; the fix now lives in #13968 (replacing #13511), with a docs/menu.md note; added a 19-second capture of the bug and the fix. Also 2026-10-02: the repro commit now names the quattro tip at filing time (the earlier SHA was from my mirror of quattro; the parser is identical); fixed a typo. Replaced both videos. The narrated one now runs this issue's repro and the label-only/action visibility check on 3faafba2 and on the #13968 head, instead of showing #13511's branch as the fix; the short one is re-recorded on the #13968 head.

View raw text
A hand-edited `menu.jsonc` whose root is a JSON **array** instead of an object is silently turned into menu rows with ids `"0"`, `"1"`, … parented to root. That includes the user extension `~/.config/omarchy/extensions/omarchy-menu.jsonc`, which goes through the same `parseMenuJsonc`. Label-only elements stay hidden as empty submenus; only elements with an `action` show, under the user's own labels.

Reproduced on `quattro` (`3faafba2`, 2026-09-27), using the repo's own parser, stock file fetched straight from this repo, and then on the #13968 head `b2dae8db` (narrated, 43s):

https://github.com/user-attachments/assets/5075e08a-2ef3-4855-b229-eeefcd7567e9

**Repro** (run from the repo root, node ≥ 18):

```js
// repro.js
const m = require("./shell/plugins/menu/MenuModel.js");

const rows = m.parseMenuJsonc('[{"label":"should-not-appear"},{"label":"ghost-2"}]');
console.log(JSON.stringify(rows));
// expected: []   actual: two rows with id "0" and "1", parent "root"

// every other non-object root behaves correctly:
for (const s of ["{}", "null", "42", '"str"']) console.log(s, m.parseMenuJsonc(s));
// -> all []
```

**What happens.** `parseMenuJsonc` guards the parsed root with

```js
if (typeof parsed !== "object" || parsed === null) return [];
```

but `typeof [] === "object"`, so an array root passes the guard. The `for..in` loop then iterates **array indices** as entry ids, and each object element becomes a row with id `"0"`, `"1"`, … — no error anywhere.

**Blast radius.** Anyone who hand-edits `menu.jsonc` and writes the root as a list of entries (a plausible mistake — it's the natural shape for "a list of menu items") gets rows keyed by array index with zero feedback about what went wrong: label-only entries are hidden, and entries with an `action` show under their own labels. Silent wrong behavior instead of either a clean empty menu or a config error.

**Fix.** One-line guard: reject `Array.isArray(parsed)` alongside the existing checks, matching what every other non-object root already does. That fix went out as #13511 and now lives in #13968, which replaces it and also fixes #13250 and #13493. #13968 makes both cases below give no rows, and it notes in `docs/menu.md` that a file whose root is not an object contributes no entries (as #13681 does), so a user whose entries vanish can see why.

<details><summary>What an array root shows on <code>quattro</code> <code>8b4eae6</code> (parsed ids, then visible labels)</summary>

```sh
node -e 'const fs=require("fs"), m=require("./shell/plugins/menu/MenuModel.js"); const stock=m.parseMenuJsonc(fs.readFileSync("default/omarchy/omarchy-menu.jsonc","utf8")); for (const u of [`[{"label":"x"},{"label":"y"}]`, `[{"label":"Notes","action":"echo notes"}]`]) { const p=m.parseMenuJsonc(u), {items,itemOrder}=m.mergeMenuSources(stock,p); console.log(p.map(r=>r.id), p.filter(r=>m.isVisible(items,itemOrder,{},items[r.id])).map(r=>items[r.id].label)) }'
```

```text
[ '0', '1' ] []
[ '0' ] [ 'Notes' ]
```

</details>

A 19-second capture of the shipped parser on `quattro` `8b4eae6` and then with the fix in #13968 (`b2dae8db`), reading a menu file whose top level is an array: `quattro` invents rows `0` and `1`, the fix returns 0 rows, as it already does for other non-object roots:

https://github.com/user-attachments/assets/6781bd5d-d2d9-46ee-ad85-1f08ffc2f1cd

<sub>Independent verification: [our validation of this report](https://portal.reqproof.com/projects/omarchy/inbox/29#validation) (reproducer and verdict) · [the known-issue record](https://portal.reqproof.com/projects/omarchy/findings/KI-MENU-JSONC-ARRAY-ROOT) (status and tests).</sub>

---

**Updated 2026-10-01:** corrected an overstatement — label-only array elements stay hidden and only elements with an `action` show, under the user's own labels; the fix now lives in #13968 (replacing #13511), with a `docs/menu.md` note; added a 19-second capture of the bug and the fix. Also 2026-10-02: the repro commit now names the `quattro` tip at filing time (the earlier SHA was from my mirror of `quattro`; the parser is identical); fixed a typo. Replaced both videos. The narrated one now runs this issue's repro and the label-only/`action` visibility check on `3faafba2` and on the #13968 head, instead of showing #13511's branch as the fix; the short one is re-recorded on the #13968 head.
  • 5075e08a-2ef3-4855-b229-eeefcd7567e9
  • 6781bd5d-d2d9-46ee-ad85-1f08ffc2f1cd

Validation

AI validator claude-code (claude-opus-5.5) · evaluated revision 5 at product commit 1e43373287af · sealed 2026-10-04T21:45:44Z

Validator's summary (commentary, not evidence)

Re-validation of omacom/omarchy#13492 (latest revision, edited 2026-10-02) on quattro-proof 1e433732 (upstream quattro 035ce29f product code plus the proof layer). The menu parser is unchanged since the previous validation (311d29c7..1e433732 merges upstream 035ce29f, whose only menu changes are two data rows in the stock omarchy-menu.jsonc (Install/Remove > Service > Microsoft), the image-picker helper bin/omarchy-menu-images and one display property on the bar's menu button; Menu.qml, MenuModel.js, ConfirmDialog.qml and omarchy-menu-select are byte-identical, and the rest is lock-screen and other non-menu upstream changes plus proof-layer records) and the result holds. A menu.jsonc whose top level is a JSON array passes the root guard (typeof [] is 'object'), so its elements become rows with ids '0', '1', and so on. SW-REQ-260922-3T3F says non-object JSON yields an empty or skipped item set. The reproducer (parser level) gets two rows instead of none; null, number and object roots behave (controls). Real entry path, checked separately in the running shell: an array-rooted user extension file with one element that has an action adds a root-menu row 'Notes' (id '0') on this baseline, and the label-only element stays hidden, as the latest revision now says; with #13968 merged the file contributes no rows. The baseline tracks the defect as KI-MENU-JSONC-ARRAY-ROOT (open). The report names #13968 as the fix (replacing #13511); #13681 also passes the reproducer. The reproducer was re-run on 1e433732 with identical output; the live-shell runs are reused from 20d4e6ae (author-recorded): Menu.qml, MenuModel.js and omarchy-menu-select are unchanged since then apart from comment-only trace lines, and the two stock-menu rows added since (Install/Remove > Service > Microsoft) are outside what these runs exercise.

  • Violates SW-REQ-260922-3T3F: a top-level JSON array root renders its elements as menu rows keyed '0', '1', ... instead of yielding no items

    Engine: engine-executed run exited 1 and showed the defect signal

    What “confirmed” means here: the requirement exists and the engine reproduced the defect. That the reproducer exercises THIS requirement is the validator's argument (see rationale).

    Evidence: requirement SW-REQ-260922-3T3F · file test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh

    Validator's rationale (commentary): SW-REQ-260922-3T3F: 'Unparseable input (after stripping), non-object JSON, and non-object entries yield an empty or skipped item set.' An array root is non-object JSON; the reproducer asserts no rows and gets two.

  • ConfirmedReproducible

    Reproducible: parseMenuJsonc('[{...},{...}]') returns two rows with ids '0' and '1'

    Engine: engine-executed run exited 1 and showed the defect signal

    Evidence: file test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh

  • ConfirmedAffects a componentmenu

    Affects the menu component

    Engine: confirmed violation of SW-REQ-260922-3T3F owned by menu

    Evidence: requirement SW-REQ-260922-3T3F · file test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh

  • Matches known issue KI-MENU-JSONC-ARRAY-ROOT

    Engine: the known issue exists; whether the report duplicates it is settled by a human disposition; it affects the mapped requirement(s) SW-REQ-260922-3T3F

    Evidence: known_issue KI-MENU-JSONC-ARRAY-ROOT · requirement SW-REQ-260922-3T3F

    Validator's rationale (commentary): The baseline KI records exactly this defect (phantom rows from an array root) and links #13492.

Engine reference checks at 1e43373287af (9/9 ok)
  • okcommit1e43373287afbd6dca4674ad463d5b5db7812358
  • okbase commit1e43373287afbd6dca4674ad463d5b5db7812358
  • okbase ancestry1e43373287afbd6dca4674ad463d5b5db7812358
  • okproduct code untouchedthis report's reproducer branch
  • okbranchthis report's reproducer branch
  • okfiletest/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh
  • okrequirementSW-REQ-260922-3T3F
  • okcomponentmenu
  • okknown issueKI-MENU-JSONC-ARRAY-ROOT

The engine's own check, at validation time, that the commits, the reproducer branch (touching only reproducer files), the reproducer files and every mapped requirement / component / known issue really exist.

10 earlier checks — superseded by the newest one above

Each new check of the report replaces the previous one; only the newest result above counts. Earlier results are kept for the record.

  • Reproduced— checked report revision 5 on Oct 4, 2026; earlier check, superseded
  • Reproduced— checked report revision 5 on Oct 3, 2026; earlier check, superseded
  • Reproduced— checked report revision 5 on Oct 3, 2026; earlier check, superseded
  • Reproduced— checked report revision 5 on Oct 2, 2026; earlier check, superseded
  • Reproduced— checked report revision 5 on Oct 2, 2026; earlier check, superseded
  • Reproduced— checked report revision 5 on Oct 2, 2026; earlier check, superseded
  • Reproduced— checked report revision 3 on Oct 2, 2026; earlier check, superseded
  • Reproduced— checked report revision 1 on Oct 1, 2026; earlier check, superseded
  • Refuted— checked report revision 1 on Oct 1, 2026; earlier check, superseded
  • Reproduced— checked report revision 1 on Sep 29, 2026; earlier check, superseded

Validator-authored hypothesis. Each claim's verification is engine-computed: confirmed only from an engine-executed reproducer run recorded in this record, refuted when recorded evidence contradicts it. Prose is commentary. The disposition of the report is a human decision.

Reproducer

engine-runexit 1 · expected exit 0 · defect exit 1 · 29 ms · 2026-10-04T21:45:44Z→ showed the defect
test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.shon this report's reproducer branch @ 1e43373287
sh test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh

Setup failure: exit 2 means the reproducer could not set up (e.g. the product did not build) — such a run decides nothing.

Defect signal: output contains DEFECT:

Matched lines (engine, over the full output)
DEFECT: top-level array root yields no rows -- input "[{\"label\":\"should-not-appear\"},{\"label\":\"ghost-2\"}]" -> rows ["0=\"should-not-appear\"","1=\"ghost-2\""] (expected [])
Observed output · 450 bytes
DEFECT: top-level array root yields no rows -- input "[{\"label\":\"should-not-appear\"},{\"label\":\"ghost-2\"}]" -> rows ["0=\"should-not-appear\"","1=\"ghost-2\""] (expected [])
ok: control: null root yields no rows -- input "null" -> rows []
ok: control: number root yields no rows -- input "42" -> rows []
ok: control: object root still parses -- input "{\"a\": {\"label\": \"A\"}}" -> rows ["a=\"A\""]
RESULT: 1 case(s) violate the requirement

Needs: linux · node >= 18 on PATH (the script exits 2 with SETUP: otherwise); nothing to build: MenuModel.js is plain JavaScript loaded straight from the worktree · ran on linux/amd64

Author's comparative runs — commentary, not engine evidence
  • author-recordedon this baseline with upstream PR omacom/omarchy#13968 head b2dae8db merged · exit 0
    sh test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh
    RESULT: all cases parse to the declared entries
  • author-recordedon upstream PR omacom/omarchy#13681 head 6acbace1 (its MenuModel.js over this baseline) · exit 0
    sh test/reports/report-cmulr8j7z0hy31gw4pne8v6u4.sh
    RESULT: all cases parse to the declared entries
  • author-recordedon live shell: Quickshell 0.3.1 on a headless sway compositor, the tree's own shell and stock menu, the user extension file ~/.config/omarchy/extensions/omarchy-menu.jsonc read by the shell's FileView; rows read back through a read-only IPC probe injected into a private copy; this baseline [run on 20d4e6ae, same menu code] · exit 1
    user extension: [{"label": "Notes", "action": "echo notes"}, {"label": "ghost-label-only"}]
    root menu gains row id=0 label="Notes"; the label-only element is not shown
  • author-recordedon live shell: Quickshell 0.3.1 on a headless sway compositor, the tree's own shell and stock menu, the user extension file ~/.config/omarchy/extensions/omarchy-menu.jsonc read by the shell's FileView; rows read back through a read-only IPC probe injected into a private copy; this baseline with upstream PR omacom/omarchy#13968 head b2dae8db merged [run on 20d4e6ae, same menu code] · exit 0
    same user extension file
    root menu shows only the 10 stock rows; the array file contributes none

Linked

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.