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

Laptop display never re-enabled after unplugging an external monitor: Hyprland's FALLBACK output is counted as an external display

Reported by HritwikSinghal · filed Aug 24, 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

Laptop display never re-enabled after unplugging an external monitor: Hyprland's FALLBACK output is counted as an external displayrevision 1 · captured 2026-09-28 21:24 UTC via poll

What happens

  1. Dock a laptop to an external monitor.
  2. Turn the laptop panel off (SUPER + CTRL + Delete, or Hardware -> Laptop Display).
  3. Unplug the external monitor.

The laptop panel is never re-enabled. The machine is left with no display at all until the next login, where omarchy-recover-internal-monitor.service clears the toggle.

This appears to be what #7228 and #7209 are reporting. #7380 is the same virtual output crashing omarchy-shell.

Root cause

omarchy-hyprland-monitor-external-active decides "is an external monitor active?" purely from output names:

hyprctl monitors all -j | jq -e '.[] | select(.name | test("^(eDP|LVDS|DSI)-") | not) | select(.disabled == false)'

But Hyprland runs outputs that have no display behind them. Its FallbackStateKeeper (src/state/FallbackState.cpp, added in 0.56.0) creates one named literally FALLBACK, through the headless backend, the moment the set of enabled monitors becomes empty:

m_listeners.monitorRemoved = Event::bus()->m_events.monitor.removed.listen([this](PHLMONITOR removed) {
    if (State::monitorState()->monitors().size() > 1)
        return;
    ...
    Log::logger->log(Log::DEBUG, "[FallbackStateKeeper] Monitor {} removed, entering fallback", removed->m_name);
    setFallbackActive(true);
});

monitors() is m_monitors (enabled only), distinct from allMonitors()/m_realMonitors (which includes disabled ones). That is the crux: an internal panel sitting at disabled = true does not count, so unplugging the external display in step 3 empties the enabled set and the fallback fires.

FALLBACK then reports disabled: false and matches none of the internal-panel prefixes, so the helper answers "an external monitor is active" -- and every caller that uses it to decide whether re-enabling the internal panel is safe backs off:

# bin/omarchy-hyprland-monitor-internal
recover() {
  omarchy-hyprland-monitor-external-active && return 0   # <-- bails here
  omarchy-hyprland-toggle-enabled $TOGGLE || return 0
  omarchy-hyprland-toggle $TOGGLE off                    # never reached
  wake
}

Same guard in omarchy-hyprland-monitor-clamshell's enable_internal(), and in the watcher's sync_poll_state(). omarchy-hyprland-monitor-watch fires correctly on monitorremoved and retries at 1s, 3s and 7s -- every attempt bails at the same line, so the toggle flag is never cleared.

Only the next login recovers, because omarchy-recover-internal-monitor.service asks omarchy-hw-external-monitors instead, which reads DRM state and cannot be fooled this way.

Reproducing the false positive without unplugging anything

hyprctl output create headless makes a virtual output through the same headless backend:

$ hyprctl output create headless
$ hyprctl monitors all -j | jq -r '.[] | "\(.name) disabled=\(.disabled)"'
eDP-1 disabled=false
HEADLESS-1 disabled=false

$ omarchy-hw-external-monitors; echo "DRM check      -> $?"   # 1 = nothing connected, correct
DRM check      -> 1
$ omarchy-hyprland-monitor-external-active; echo "name check -> $?"   # 0 = "external active", wrong
name check -> 0

This is a regression

omarchy 3.8.5 guarded recovery on the hardware check, so it was immune:

# omarchy 3.8.5, bin/omarchy-hyprland-monitor-internal
recover() {
  if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled $TOGGLE; then
    omarchy-hyprland-toggle $TOGGLE off
  fi
}

omarchy-hyprland-monitor-external-active did not exist in 3.8.5; neither did omarchy-hyprland-monitor-clamshell. off() regressed the same way -- 3.8.5 guarded "Can't disable the only active display" with ! omarchy-hw-external-monitors too.

Worth noting that 96afc684 (2026-04-23) moved this guard onto the hardware check deliberately, titled "Move to a hardware check for external monitors instead to deal with recover timing". The quattro rewrite moved it back onto the name check.

Two things I checked and ruled out, in case they come up:

  • Hyprland 0.55 -> 0.56 is not the trigger. 0.55.4 also entered an unsafe state and enabled a HEADLESS-1 output when the last enabled monitor went away (prepareFallbackOutput/enterUnsafeState in src/Compositor.cpp), so a name test would have been fooled there too. 0.56 only renamed it to FALLBACK and made it on-demand rather than created at startup.
  • f4b832eb (`hyprctl monitors` -> `hyprctl monitors all`) is not the trigger. I ran the helper's exact jq both ways against a live virtual output; both report "external active". No difference for this bug.

Suggested fix

A real display has a DRM connector; a virtual output has none. Cross-check candidates against sysfs, which restores the hardware truth recover() used to have while keeping the monitors all behaviour f4b832eb needed for mirrors (a mirrored external has a connected connector, so it keeps counting).

PR: fixes the helper in one place, so recover(), off(), clamshell's enable_internal()/disable_internal() and the watcher's poll are all corrected at once. Includes a test that fails on the current helper at its first assertion.

Environment

  • omarchy-dev 4.0.0.r1816.g43bfe9b-1
  • Hyprland 0.56.2
  • Dell XPS, eDP-1 internal panel + DP-1 external
View raw text
### What happens

1. Dock a laptop to an external monitor.
2. Turn the laptop panel off (`SUPER + CTRL + Delete`, or Hardware -> Laptop Display).
3. Unplug the external monitor.

The laptop panel is never re-enabled. The machine is left with **no display at all** until the next login, where `omarchy-recover-internal-monitor.service` clears the toggle.

This appears to be what #7228 and #7209 are reporting. #7380 is the same virtual output crashing `omarchy-shell`.

### Root cause

`omarchy-hyprland-monitor-external-active` decides "is an external monitor active?" purely from output *names*:

```bash
hyprctl monitors all -j | jq -e '.[] | select(.name | test("^(eDP|LVDS|DSI)-") | not) | select(.disabled == false)'
```

But Hyprland runs outputs that have no display behind them. Its `FallbackStateKeeper` (`src/state/FallbackState.cpp`, added in 0.56.0) creates one named literally `FALLBACK`, through the headless backend, the moment the set of **enabled** monitors becomes empty:

```cpp
m_listeners.monitorRemoved = Event::bus()->m_events.monitor.removed.listen([this](PHLMONITOR removed) {
    if (State::monitorState()->monitors().size() > 1)
        return;
    ...
    Log::logger->log(Log::DEBUG, "[FallbackStateKeeper] Monitor {} removed, entering fallback", removed->m_name);
    setFallbackActive(true);
});
```

`monitors()` is `m_monitors` (enabled only), distinct from `allMonitors()`/`m_realMonitors` (which includes disabled ones). **That is the crux:** an internal panel sitting at `disabled = true` does not count, so unplugging the external display in step 3 empties the enabled set and the fallback fires.

`FALLBACK` then reports `disabled: false` and matches none of the internal-panel prefixes, so the helper answers "an external monitor is active" -- and every caller that uses it to decide whether re-enabling the internal panel is safe backs off:

```bash
# bin/omarchy-hyprland-monitor-internal
recover() {
  omarchy-hyprland-monitor-external-active && return 0   # <-- bails here
  omarchy-hyprland-toggle-enabled $TOGGLE || return 0
  omarchy-hyprland-toggle $TOGGLE off                    # never reached
  wake
}
```

Same guard in `omarchy-hyprland-monitor-clamshell`'s `enable_internal()`, and in the watcher's `sync_poll_state()`. `omarchy-hyprland-monitor-watch` fires correctly on `monitorremoved` and retries at 1s, 3s and 7s -- every attempt bails at the same line, so the toggle flag is never cleared.

Only the next login recovers, because `omarchy-recover-internal-monitor.service` asks `omarchy-hw-external-monitors` instead, which reads DRM state and cannot be fooled this way.

### Reproducing the false positive without unplugging anything

`hyprctl output create headless` makes a virtual output through the same headless backend:

```console
$ hyprctl output create headless
$ hyprctl monitors all -j | jq -r '.[] | "\(.name) disabled=\(.disabled)"'
eDP-1 disabled=false
HEADLESS-1 disabled=false

$ omarchy-hw-external-monitors; echo "DRM check      -> $?"   # 1 = nothing connected, correct
DRM check      -> 1
$ omarchy-hyprland-monitor-external-active; echo "name check -> $?"   # 0 = "external active", wrong
name check -> 0
```

### This is a regression

omarchy 3.8.5 guarded recovery on the hardware check, so it was immune:

```bash
# omarchy 3.8.5, bin/omarchy-hyprland-monitor-internal
recover() {
  if ! omarchy-hw-external-monitors && omarchy-hyprland-toggle-enabled $TOGGLE; then
    omarchy-hyprland-toggle $TOGGLE off
  fi
}
```

`omarchy-hyprland-monitor-external-active` did not exist in 3.8.5; neither did `omarchy-hyprland-monitor-clamshell`. `off()` regressed the same way -- 3.8.5 guarded "Can't disable the only active display" with `! omarchy-hw-external-monitors` too.

Worth noting that 96afc684 (2026-04-23) moved this guard onto the hardware check *deliberately*, titled "Move to a hardware check for external monitors instead to deal with recover timing". The quattro rewrite moved it back onto the name check.

Two things I checked and ruled out, in case they come up:

- **Hyprland 0.55 -> 0.56 is not the trigger.** 0.55.4 also entered an unsafe state and enabled a `HEADLESS-1` output when the last enabled monitor went away (`prepareFallbackOutput`/`enterUnsafeState` in `src/Compositor.cpp`), so a name test would have been fooled there too. 0.56 only renamed it to `FALLBACK` and made it on-demand rather than created at startup.
- **f4b832eb (`hyprctl monitors` -> `hyprctl monitors all`) is not the trigger.** I ran the helper's exact jq both ways against a live virtual output; both report "external active". No difference for this bug.

### Suggested fix

A real display has a DRM connector; a virtual output has none. Cross-check candidates against sysfs, which restores the hardware truth `recover()` used to have while keeping the `monitors all` behaviour f4b832eb needed for mirrors (a mirrored external has a connected connector, so it keeps counting).

PR: fixes the helper in one place, so `recover()`, `off()`, `clamshell`'s `enable_internal()`/`disable_internal()` and the watcher's poll are all corrected at once. Includes a test that fails on the current helper at its first assertion.

### Environment

- `omarchy-dev` 4.0.0.r1816.g43bfe9b-1
- Hyprland 0.56.2
- Dell XPS, `eDP-1` internal panel + DP-1 external

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

  • Fixed bypr/7146· confirmed

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 08:39 UTC