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

Saved battery power profile never applied when booting on battery: omarchy-powerprofiles-set autodetect silently falls back to "ac"

Reported by kzagoris · filed Sep 21, 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

Saved battery power profile never applied when booting on battery: omarchy-powerprofiles-set autodetect silently falls back to "ac"revision 1 · captured 2026-09-28 21:23 UTC via poll

Summary

The power profile saved for battery is never applied when the machine boots already on battery. The session comes up on whatever profile power-profiles-daemon restored from its own persisted state (here performance), and nothing corrects it for as long as the machine stays unplugged — in my case 24 hours, with power-saver sitting saved and unused.

The practical effect on a laptop is that a heavy build runs at full turbo on battery when the user has explicitly asked for power-saver.

Environment

  • Omarchy 4.0.4-1
  • power-profiles-daemon 0.30-1
  • Kernel 7.2.6-arch2-Watanare-T2-3-t2
  • CPU: Intel Core i9-9980HK (MacBookPro16,1, T2)
  • Packages last updated: Sep 20 2026

Steps to reproduce

  1. In the power panel, select Power-saver while on battery. Confirm ~/.local/state/omarchy/powerprofiles/battery now contains power-saver.
  2. Ensure power-profiles-daemon's own state has a different profile, e.g. /var/lib/power-profiles-daemon/state.ini with Profile=performance.
  3. Shut down, unplug AC, and boot with no AC attached.
  4. powerprofilesctl get

Expected: power-saver (the saved battery preference).
Actual: performance.

Evidence from my system

Saved preference, written Aug 25:

$ cat ~/.local/state/omarchy/powerprofiles/battery
power-saver

The boot in question began on battery:

Sep 20 09:26:53 kernel: ACPI: AC: AC Adapter [ADP1] (off-line)

That boot ran 24 hours. journalctl -b -u power-profiles-daemon for it shows the daemon starting and no profile change at any point:

Sep 20 09:26:57 systemd[1]: Starting Power Profiles daemon...
Sep 20 09:26:57 systemd[1]: Started Power Profiles daemon.

PPD's persisted state, which it restores at startup:

# /var/lib/power-profiles-daemon/state.ini
Profile=performance

So the machine sat on performance, on battery, at full turbo (intel_pstate/no_turbo=0, max_perf_pct=100), for the entire session.

The setter itself works correctly — the preference is honored when it is actually invoked:

$ powerprofilesctl get
performance
$ omarchy-powerprofiles-set battery && powerprofilesctl get
power-saver

Analysis

Two things look relevant. I have verified the first as a latent defect; the second I can show is a real gap but have not proven fired in my specific boot.

1. autodetect fail-opens to ac

omarchy-powerprofiles-init runs at session start (default/hypr/autostart.lua:8) and calls omarchy-powerprofiles-set autodetect. The detection is:

if [[ $(busctl get-property org.freedesktop.UPower /org/freedesktop/UPower \
        org.freedesktop.UPower OnBattery 2>/dev/null) == "b true" ]]; then
  action=battery
else
  action=ac
fi

Only the exact string b true selects battery. Every other outcome — UPower not yet activatable, D-Bus not ready, a timeout, any stderr-suppressed failure — falls through to ac. There is no exit-status check and no retry.

Because most users have no ac state file (the file is written only when a profile is explicitly requested for that source), action=ac then hits the fallback at the bottom of the script and resolves to `performance`:

if [[ $action == "ac" ]] && profile_available performance; then
  profile=performance

So a failed or premature UPower query on a machine booting on battery silently produces the most aggressive profile — the opposite of the saved preference, with no error surfaced. Confirmed by simulating a failing query:

$ out=$(busctl get-property org.freedesktop.NONEXISTENT /nope org.nope Nope 2>/dev/null)
$ [[ $out == "b true" ]] && echo battery || echo ac
ac

A failed detection should be distinguishable from a successful "not on battery", and should retry or bail rather than defaulting to the highest-power profile.

2. The shell service only reacts to transitions

shell/plugins/services/battery/Service.qml:

Connections {
  target: UPower
  function onOnBatteryChanged() {
    root.checkBattery()
    root.applyPowerProfile()     // only caller
  }
}

applyPowerProfile() is reachable only from onOnBatteryChanged. There is no Component.onCompleted, and the 30s Timer calls only checkBattery(). So the shell never reconciles the profile against the current power source — it only reacts to AC↔battery changes.

That is by design, since omarchy-powerprofiles-init is supposed to cover startup. But it means the init is a single point of failure: if that one call resolves wrongly (see #1), nothing else in the running system ever corrects it. A periodic or startup reconcile in the service would make the outcome self-healing regardless of what happened at boot.

Suggested fix directions

  • In autodetect, check busctl's exit status and retry briefly; treat a failed query as unknown rather than as ac.
  • Consider making battery the safer fallback when detection fails, or refusing to change the profile at all.
  • Optionally, have Service.qml reconcile the profile on startup (Component.onCompleted) or on its existing 30s timer, so a bad boot-time resolution self-corrects.

Related

#8184 covered the older udev-based boot race, and its follow-up comment describes the quattro redesign that replaced it with omarchy-powerprofiles-init at session start. This report is about that replacement path: the init is wired up correctly, but its detection can silently resolve to ac, and nothing reconciles afterwards.

View raw text
## Summary

The power profile saved for battery is never applied when the machine **boots already on battery**. The session comes up on whatever profile `power-profiles-daemon` restored from its own persisted state (here `performance`), and nothing corrects it for as long as the machine stays unplugged — in my case 24 hours, with `power-saver` sitting saved and unused.

The practical effect on a laptop is that a heavy build runs at full turbo on battery when the user has explicitly asked for `power-saver`.

## Environment

- Omarchy 4.0.4-1
- power-profiles-daemon 0.30-1
- Kernel 7.2.6-arch2-Watanare-T2-3-t2
- CPU: Intel Core i9-9980HK (MacBookPro16,1, T2)
- Packages last updated: Sep 20 2026

## Steps to reproduce

1. In the power panel, select **Power-saver** while on battery. Confirm `~/.local/state/omarchy/powerprofiles/battery` now contains `power-saver`.
2. Ensure `power-profiles-daemon`'s own state has a different profile, e.g. `/var/lib/power-profiles-daemon/state.ini` with `Profile=performance`.
3. Shut down, unplug AC, and boot **with no AC attached**.
4. `powerprofilesctl get`

**Expected:** `power-saver` (the saved battery preference).
**Actual:** `performance`.

## Evidence from my system

Saved preference, written Aug 25:

```
$ cat ~/.local/state/omarchy/powerprofiles/battery
power-saver
```

The boot in question began on battery:

```
Sep 20 09:26:53 kernel: ACPI: AC: AC Adapter [ADP1] (off-line)
```

That boot ran 24 hours. `journalctl -b -u power-profiles-daemon` for it shows the daemon starting and **no profile change at any point**:

```
Sep 20 09:26:57 systemd[1]: Starting Power Profiles daemon...
Sep 20 09:26:57 systemd[1]: Started Power Profiles daemon.
```

PPD's persisted state, which it restores at startup:

```ini
# /var/lib/power-profiles-daemon/state.ini
Profile=performance
```

So the machine sat on `performance`, on battery, at full turbo (`intel_pstate/no_turbo=0`, `max_perf_pct=100`), for the entire session.

The setter itself works correctly — the preference is honored when it is actually invoked:

```
$ powerprofilesctl get
performance
$ omarchy-powerprofiles-set battery && powerprofilesctl get
power-saver
```

## Analysis

Two things look relevant. I have verified the first as a latent defect; the second I can show is a real gap but have not proven fired in my specific boot.

### 1. `autodetect` fail-opens to `ac`

`omarchy-powerprofiles-init` runs at session start (`default/hypr/autostart.lua:8`) and calls `omarchy-powerprofiles-set autodetect`. The detection is:

```bash
if [[ $(busctl get-property org.freedesktop.UPower /org/freedesktop/UPower \
        org.freedesktop.UPower OnBattery 2>/dev/null) == "b true" ]]; then
  action=battery
else
  action=ac
fi
```

Only the exact string `b true` selects `battery`. **Every** other outcome — UPower not yet activatable, D-Bus not ready, a timeout, any stderr-suppressed failure — falls through to `ac`. There is no exit-status check and no retry.

Because most users have no `ac` state file (the file is written only when a profile is explicitly requested for that source), `action=ac` then hits the fallback at the bottom of the script and resolves to **`performance`**:

```bash
if [[ $action == "ac" ]] && profile_available performance; then
  profile=performance
```

So a failed or premature UPower query on a machine booting on battery silently produces the most aggressive profile — the opposite of the saved preference, with no error surfaced. Confirmed by simulating a failing query:

```bash
$ out=$(busctl get-property org.freedesktop.NONEXISTENT /nope org.nope Nope 2>/dev/null)
$ [[ $out == "b true" ]] && echo battery || echo ac
ac
```

A failed detection should be distinguishable from a successful "not on battery", and should retry or bail rather than defaulting to the highest-power profile.

### 2. The shell service only reacts to transitions

`shell/plugins/services/battery/Service.qml`:

```qml
Connections {
  target: UPower
  function onOnBatteryChanged() {
    root.checkBattery()
    root.applyPowerProfile()     // only caller
  }
}
```

`applyPowerProfile()` is reachable only from `onOnBatteryChanged`. There is no `Component.onCompleted`, and the 30s `Timer` calls only `checkBattery()`. So the shell never reconciles the profile against the current power source — it only reacts to AC↔battery *changes*.

That is by design, since `omarchy-powerprofiles-init` is supposed to cover startup. But it means the init is a single point of failure: if that one call resolves wrongly (see #1), nothing else in the running system ever corrects it. A periodic or startup reconcile in the service would make the outcome self-healing regardless of what happened at boot.

## Suggested fix directions

- In `autodetect`, check `busctl`'s exit status and retry briefly; treat a failed query as unknown rather than as `ac`.
- Consider making `battery` the safer fallback when detection fails, or refusing to change the profile at all.
- Optionally, have `Service.qml` reconcile the profile on startup (`Component.onCompleted`) or on its existing 30s timer, so a bad boot-time resolution self-corrects.

## Related

#8184 covered the older udev-based boot race, and its follow-up comment describes the `quattro` redesign that replaced it with `omarchy-powerprofiles-init` at session start. This report is about that replacement path: the init is wired up correctly, but its detection can silently resolve to `ac`, and nothing reconciles afterwards.

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 16:54 UTC