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

Agents panel says "Sign-in expired" for a routine 8-hour access-token lapse and tells people to re-run `claude auth login`

Reported by meibe-ab · filed Aug 24, 2026Closed upstream on 2026-10-04 by the bot omarchybot; the closing comment says it was fixed by PR #14049. 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

Agents panel says "Sign-in expired" for a routine 8-hour access-token lapse and tells people to re-run `claude auth login`revision 1 · captured 2026-09-28 21:24 UTC via poll

The agents panel treats a lapsed access token as a lapsed sign-in. Those are different things, and the difference is a full day of validity.

~/.claude/.credentials.json holds two expiries:

  • expiresAt — the access token, good for ~8 hours
  • refreshTokenExpiresAt — the refresh token, good for ~30 days

omarchy-agent-usage-claude reads only the first one:

# bin/omarchy-agent-usage-claude, collect_limits()
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
    result["limits"] = fallback
    result["usageStatusText"] = "Sign-in expired"
    result["authHelpText"] = (
      "Claude Code's saved sign-in expired"
      + (" — showing the last known limits." if fallback else ".")
      + " Start Claude Code, or run `claude auth login`, to refresh it."
    )
    return result

So any machine left alone for more than eight hours — i.e. every morning — shows SIGN-IN EXPIRED in the hero line and is told to run claude auth login, while the stored session is still valid for weeks.

The advice is not merely redundant, it is counterproductive: claude auth login mints a brand-new OAuth session and rotates the credentials. People who follow the panel's instruction end up with a fresh login recorded on their account every single day, and reasonably conclude Claude Code is logging them out. Simply starting Claude Code refreshes the access token silently and changes nothing else.

Evidence from a machine that was never signed out

expiresAt              = 1787624562658  -> 2026-08-25 04:22:42   (8h after the file was written)
refreshTokenExpiresAt  = 1790070218658  -> 2026-09-22 11:43:38   (30d from 2026-08-23 11:43)

The access token was reissued at 20:22 today with refreshTokenExpiresAt unchanged, i.e. from the refresh token, not a new login. That credential survived a reboot and is 30 days from expiry. The panel would still have said "Sign-in expired" for the eight-hour gap before it.

Steps to reproduce

  1. Sign in to Claude Code normally.
  2. Leave the machine for more than 8 hours (or set expiresAt in ~/.claude/.credentials.json to a past timestamp, leaving refreshTokenExpiresAt in the future).
  3. omarchy-agent-usage-claude → usageStatusText: "Sign-in expired", help text recommending claude auth login.

Expected

A stale access token with a live refresh token is normal, so the panel should say the limits are stale and point at the thing that actually fixes it — starting Claude Code. claude auth login should be suggested only once refreshTokenExpiresAt has also passed, which is a genuine sign-out.

Proposed fix

oauth_login() already parses the credential file, so the refresh expiry costs one extra key. Patch against 4.0.0-1:

--- a/bin/omarchy-agent-usage-claude
+++ b/bin/omarchy-agent-usage-claude
@@ -581,16 +581,21 @@
-def oauth_login(claude_dir: Path) -> tuple[str, int, str]:
+def oauth_login(claude_dir: Path) -> tuple[str, int, int, str]:
   try:
     data = json.loads((claude_dir / ".credentials.json").read_text(encoding="utf-8"))
   except Exception:
-    return "", 0, ""
+    return "", 0, 0, ""
   login = data.get("claudeAiOauth")
   if not isinstance(login, dict):
-    return "", 0, ""
+    return "", 0, 0, ""
   plan = plan_label(str(login.get("rateLimitTier") or ""), str(login.get("subscriptionType") or ""))
-  return str(login.get("accessToken") or ""), number(login.get("expiresAt")), plan
+  return (
+    str(login.get("accessToken") or ""),
+    number(login.get("expiresAt")),
+    number(login.get("refreshTokenExpiresAt")),
+    plan,
+  )
@@ -791,7 +796,7 @@
-def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
+def collect_limits(access_token: str, expires_at_ms: int, refresh_expires_at_ms: int, force: bool) -> dict[str, Any]:
@@ -812,6 +817,19 @@
   if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
     result["limits"] = fallback
+    # A lapsed access token is routine, not a sign-out: it lives about eight
+    # hours, and the CLI mints a new one from the refresh token the next time
+    # it runs. Only a lapsed *refresh* token is a sign-in a person must redo,
+    # and saying so either way sends people to `claude auth login` nightly --
+    # which does not restore anything the CLI would not have restored itself.
+    if refresh_expires_at_ms > time.time() * 1000:
+      result["usageStatusText"] = "Limits paused"
+      result["authHelpText"] = (
+        "Claude Code's access token has lapsed"
+        + (" — showing the last known limits." if fallback else ".")
+        + " Start Claude Code to refresh it; signing in again is not needed."
+      )
+      return result
     result["usageStatusText"] = "Sign-in expired"
@@ -879,8 +897,8 @@
-  access_token, expires_at_ms, plan = oauth_login(claude_dir)
-  limits = collect_limits(access_token, expires_at_ms, args.force)
+  access_token, expires_at_ms, refresh_expires_at_ms, plan = oauth_login(claude_dir)
+  limits = collect_limits(access_token, expires_at_ms, refresh_expires_at_ms, args.force)

Verified locally with planted credential files:

casebeforeafter
access token lapsed, refresh liveSign-in expired → "run claude auth login"Limits paused → "Start Claude Code to refresh it; signing in again is not needed."
access and refresh lapsedSign-in expiredSign-in expired (unchanged)
both livelimits fetched normallylimits fetched normally (unchanged)

The cached-limits fallback introduced by #6795 still applies in both branches, so the last known numbers keep rendering either way. The collect_limits tests added in #6795 will need the extra argument, plus one new case for a live refresh token.

This is a follow-up to #6795, which correctly stopped the section from vanishing silently — the status text it chose is just wrong for the common case.

System

  • omarchy 4.0.0-1 (4.0.0.alpha), Arch, Hyprland
  • Claude Code 2.1.241, Max plan

Filed by Claude Opus 5 via Claude Code.

View raw text
The agents panel treats a lapsed **access** token as a lapsed **sign-in**. Those are different things, and the difference is a full day of validity.

`~/.claude/.credentials.json` holds two expiries:

- `expiresAt` — the access token, good for **~8 hours**
- `refreshTokenExpiresAt` — the refresh token, good for **~30 days**

`omarchy-agent-usage-claude` reads only the first one:

```python
# bin/omarchy-agent-usage-claude, collect_limits()
if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
    result["limits"] = fallback
    result["usageStatusText"] = "Sign-in expired"
    result["authHelpText"] = (
      "Claude Code's saved sign-in expired"
      + (" — showing the last known limits." if fallback else ".")
      + " Start Claude Code, or run `claude auth login`, to refresh it."
    )
    return result
```

So any machine left alone for more than eight hours — i.e. every morning — shows **SIGN-IN EXPIRED** in the hero line and is told to run `claude auth login`, while the stored session is still valid for weeks.

The advice is not merely redundant, it is counterproductive: `claude auth login` mints a brand-new OAuth session and rotates the credentials. People who follow the panel's instruction end up with a fresh login recorded on their account every single day, and reasonably conclude Claude Code is logging them out. Simply starting Claude Code refreshes the access token silently and changes nothing else.

### Evidence from a machine that was never signed out

```
expiresAt              = 1787624562658  -> 2026-08-25 04:22:42   (8h after the file was written)
refreshTokenExpiresAt  = 1790070218658  -> 2026-09-22 11:43:38   (30d from 2026-08-23 11:43)
```

The access token was reissued at 20:22 today with `refreshTokenExpiresAt` **unchanged**, i.e. from the refresh token, not a new login. That credential survived a reboot and is 30 days from expiry. The panel would still have said "Sign-in expired" for the eight-hour gap before it.

### Steps to reproduce

1. Sign in to Claude Code normally.
2. Leave the machine for more than 8 hours (or set `expiresAt` in `~/.claude/.credentials.json` to a past timestamp, leaving `refreshTokenExpiresAt` in the future).
3. `omarchy-agent-usage-claude` → `usageStatusText: "Sign-in expired"`, help text recommending `claude auth login`.

### Expected

A stale access token with a live refresh token is normal, so the panel should say the limits are stale and point at the thing that actually fixes it — starting Claude Code. `claude auth login` should be suggested only once `refreshTokenExpiresAt` has also passed, which is a genuine sign-out.

### Proposed fix

`oauth_login()` already parses the credential file, so the refresh expiry costs one extra key. Patch against 4.0.0-1:

```diff
--- a/bin/omarchy-agent-usage-claude
+++ b/bin/omarchy-agent-usage-claude
@@ -581,16 +581,21 @@
-def oauth_login(claude_dir: Path) -> tuple[str, int, str]:
+def oauth_login(claude_dir: Path) -> tuple[str, int, int, str]:
   try:
     data = json.loads((claude_dir / ".credentials.json").read_text(encoding="utf-8"))
   except Exception:
-    return "", 0, ""
+    return "", 0, 0, ""
   login = data.get("claudeAiOauth")
   if not isinstance(login, dict):
-    return "", 0, ""
+    return "", 0, 0, ""
   plan = plan_label(str(login.get("rateLimitTier") or ""), str(login.get("subscriptionType") or ""))
-  return str(login.get("accessToken") or ""), number(login.get("expiresAt")), plan
+  return (
+    str(login.get("accessToken") or ""),
+    number(login.get("expiresAt")),
+    number(login.get("refreshTokenExpiresAt")),
+    plan,
+  )
@@ -791,7 +796,7 @@
-def collect_limits(access_token: str, expires_at_ms: int, force: bool) -> dict[str, Any]:
+def collect_limits(access_token: str, expires_at_ms: int, refresh_expires_at_ms: int, force: bool) -> dict[str, Any]:
@@ -812,6 +817,19 @@
   if expires_at_ms > 0 and expires_at_ms <= time.time() * 1000:
     result["limits"] = fallback
+    # A lapsed access token is routine, not a sign-out: it lives about eight
+    # hours, and the CLI mints a new one from the refresh token the next time
+    # it runs. Only a lapsed *refresh* token is a sign-in a person must redo,
+    # and saying so either way sends people to `claude auth login` nightly --
+    # which does not restore anything the CLI would not have restored itself.
+    if refresh_expires_at_ms > time.time() * 1000:
+      result["usageStatusText"] = "Limits paused"
+      result["authHelpText"] = (
+        "Claude Code's access token has lapsed"
+        + (" — showing the last known limits." if fallback else ".")
+        + " Start Claude Code to refresh it; signing in again is not needed."
+      )
+      return result
     result["usageStatusText"] = "Sign-in expired"
@@ -879,8 +897,8 @@
-  access_token, expires_at_ms, plan = oauth_login(claude_dir)
-  limits = collect_limits(access_token, expires_at_ms, args.force)
+  access_token, expires_at_ms, refresh_expires_at_ms, plan = oauth_login(claude_dir)
+  limits = collect_limits(access_token, expires_at_ms, refresh_expires_at_ms, args.force)
```

Verified locally with planted credential files:

| case | before | after |
|---|---|---|
| access token lapsed, refresh live | `Sign-in expired` → "run `claude auth login`" | `Limits paused` → "Start Claude Code to refresh it; signing in again is not needed." |
| access **and** refresh lapsed | `Sign-in expired` | `Sign-in expired` (unchanged) |
| both live | limits fetched normally | limits fetched normally (unchanged) |

The cached-limits fallback introduced by #6795 still applies in both branches, so the last known numbers keep rendering either way. The `collect_limits` tests added in #6795 will need the extra argument, plus one new case for a live refresh token.

This is a follow-up to #6795, which correctly stopped the section from vanishing silently — the status text it chose is just wrong for the common case.

### System

- omarchy 4.0.0-1 (4.0.0.alpha), Arch, Hyprland
- Claude Code 2.1.241, Max plan

---
Filed 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.