Background daemon posts spurious "needs re-authentication" notification when a proactive OAuth refresh fails during macOS sleep/PowerNap dark-wake (transient, self-heals, auth never actually invalid)

Open 💬 0 comments Opened Jul 3, 2026 by SeanLF

Summary

On macOS, the background daemon (claude daemon run) runs a proactive, scheduled OAuth refresh (~every 8h — not triggered by a 401). During system sleep / PowerNap dark-wakes, this refresh intermittently fails with a false no token found and the daemon immediately posts a user-facing macOS notification:

osascript -e 'display notification "Your Claude assistant needs re-authentication" with title "Claude"'

But authentication was never actually lost. The refresh recovers on its own with no user action (token refreshed via keychain re-check retry), interactive claude sessions keep working throughout, and the stored credential is never invalidated. The notification is a false alarm for a transient, self-healing condition — and it is never retracted.

This only ever happens while the machine is asleep, never during active use, which is why it reads as a "nightly" nag.

The bug (independent of exact mechanism)

The core defect is simple and doesn't depend on the underlying cause:

A proactive refresh is opportunistic — the existing access token is still valid when it runs. If that opportunistic refresh transiently fails, the daemon escalates it to a user-facing "you must re-authenticate" alert instead of just retrying silently. Auth is fine; the user is told it isn't.

What we could and couldn't confirm about the cause

We have three logged occurrences. They do not share one clean mechanism, so we're only claiming what the evidence supports:

Occurrence A

05:53:48Z  auth: proactive refresh starting
05:54:18Z  auth: proactive refresh failed, signalling re-auth required   (+30s)
05:54:18Z  auth: headless daemon cannot complete OAuth — run `claude auth login` to refresh
05:54:18Z  auth: no token found, will re-check keychain every 30s
05:57:01Z  auth: token refreshed via keychain re-check retry             ← self-healed (~3 min)

Occurrence B (with a passive capturer running, so we have keychain + wake state)

07:35:33Z  auth: proactive refresh starting        ← fires on a PowerNap DarkWake (03:35 local)
           (machine returns to deep idle)
07:50:35Z  auth: proactive refresh failed, signalling re-auth required   (+15 min)
07:50:35Z  auth: browser login skipped (API key auth available)
07:50:35Z  auth: no token found, will re-check keychain every 30s
12:45:38Z  auth: token refreshed via keychain re-check retry             ← self-healed (~5 h, at full wake)

For Occurrence B, at the moment of failure:

  • pmset -g log showed the starting and failed lines landing on two separate DarkWakes, ~15 min apart (the machine slept between them).
  • The Claude Code-credentials keychain item's mdat was 20260706232414Zstable for ~8.4h before the failure and unchanged through it. Nothing was writing the keychain, so there was no concurrent token rotation and no lock contention.
  • Concurrent interactive sessions were unaffected; the token was valid the whole time.

Occurrence C (build 2.1.205; reconstructed from ~/.claude/daemon.log + pmset -g log, no passive capturer). Times local EDT:

04:24:09  auth: proactive refresh succeeded, next scheduled in 28560s (~7.9h → 08:33)
08:33:38  pmset: DarkWake from Deep Idle (on battery, SleepService 180s window, returning to sleep)
08:33:38  auth: proactive refresh starting        ← fires into the dark-wake / back-to-sleep transition
08:34:08  auth: proactive refresh failed, signalling re-auth required   (+30s)
08:34:08  auth: no token found, will re-check keychain every 30s        (notification posted)
08:53:42  auth: token refreshed via keychain re-check retry             ← self-healed (~19 min)

Occurrence C matches Occurrence A's ~30s startingfailed timing on a single dark-wake (unlike B's two-DarkWake, 15-min gap), reinforcing that more than one failure path resolves to the same no token found. It also directly demonstrates the stuck status-file half of the bug: daemon-auth-status.json was present and stuck at auth_required (stamped 08:34:08) and remained unreconciled 2h+ after the 08:53 self-heal — the daemon cleared the token internally but never reset the user-facing flag (see fix #4).

Conclusion on cause: the failure is not a concurrent-refresh / keychain-lock race (Occurrence B rules that out). It is tied to the constrained sleep / PowerNap dark-wake execution environment — the proactive refresh can't complete and reports a false empty read. The precise micro-mechanism (HTTP suspended mid-flight vs. keychain unavailable during dark-wake) is not fully pinned; the differing 30s-vs-15min failure timing across occurrences suggests more than one failure path resolving to the same no token found.

Also note: the advertised re-check keychain every 30s retry does not reliably recover during dark-wake — in Occurrence B recovery only happened ~5h later at full wake.

Steps to reproduce

This is environmental and probabilistic (it needs sleep/dark-wake to coincide with a due proactive refresh), so it's not deterministic — but the conditions and how to confirm it are:

Preconditions

  • macOS on Apple Silicon, PowerNap enabled (pmset -g | grep powernap1).
  • Claude Code (native installer) with its background daemon running (claude daemon run), signed in.
  • Machine left asleep on battery long enough for PowerNap dark-wakes to occur while an ~8h proactive refresh timer comes due (typically overnight).

Observe

  1. Leave the Mac asleep overnight. In the morning, look for the "Your Claude assistant needs re-authentication" notification (attributed to Script Editor) in Notification Center.
  2. grep -E "auth:" ~/.claude/daemon.log — find the proactive refresh startingproactive refresh failed, signalling re-auth requiredno token found → later token refreshed via keychain re-check retry sequence.
  3. Correlate the starting/failed timestamps with pmset -g log | grep -i darkwake.
  4. Confirm auth was never actually lost: security find-generic-password -s "Claude Code-credentials" -g shows the item's mdat unchanged from hours before the failure, and interactive claude sessions worked fine across it. If both hold, you hit this bug (a false alarm), not a real logout.

User-side mitigation: sudo pmset -a powernap 0 removes the dark-wake trigger and stops the nag entirely.

Our take on fixing it

Ordered by value / robustness (the first doesn't depend on nailing the cause):

  1. Never fire a re-auth notification while the current access token is still within its validity window. A proactive refresh is an optimization; its failure when the existing token hasn't expired should be a silent retry, full stop. This alone eliminates the user-visible symptom.
  2. Debounce. If you do surface it, wait for the re-check keychain every 30s retry to fail one or two more times first — don't alert on the first empty read.
  3. Don't run/escalate a proactive refresh during a dark-wake. Detect the constrained wake state and defer the hard refresh (and any re-auth signalling) to full wake.
  4. Reconcile on recovery. On a successful re-check, withdraw the delivered notification and reset any status file. (Directly evidenced in Occurrence C: daemon-auth-status.json stayed auth_required for 2h+ after the token self-healed.)

Environment

  • macOS (Apple Silicon), PowerNap enabled.
  • Native-installer Claude Code binary running as the background daemon. (A separately-installed Claude desktop app ships its own bundled Claude Code build; both share ~/.claude and the same keychain refresh token.)

Note on the earlier version of this report

This issue was originally filed attributing the failure to a concurrent token-rotation / keychain-cache stale-read race (the mechanism documented in #24317). Direct capture of the keychain and wake state during a live occurrence (Occurrence B above) did not support that — there was no concurrent keychain write — so the report has been rewritten to claim only what the evidence shows. It also reported daemon-auth-status.json getting stuck at auth_required after recovery; on the build captured 2026-07-07 (2.1.202) that file happened to be absent after recovery, which we tentatively read as possibly version-fixed — but Occurrence C on 2.1.205 (a newer build) shows it present and still stuck at auth_required hours after the token self-healed, so it is not fixed; its 07-07 absence was incidental. The spurious notification persists.

Related

  • #24317 (concurrent-session OAuth refresh-token race — superficially similar no token found symptom, but a different mechanism from this daemon/dark-wake case)
  • #37203, #37324 (background-agent / concurrent-subagent auth loss — permanent variant, no notification)
  • #37678 (RFC: token-refresh serialization / token broker)
  • #41511 (macOS notifications attributed to "Script Editor" — the notification-UX half)

View original on GitHub ↗