[BUG] Remote Control bridge enters terminal "failed" state after JWT refresh 401 — never retries; concurrent-session token-refresh race triggers it

Open 💬 0 comments Opened Jun 11, 2026 by TheRealJadenKwek

Summary

Sessions with Remote Control enabled eventually become permanently unreachable from claude.ai web / mobile, even though the Mac is awake, plugged in, online, and the local session is healthy. Logs show the cause: when the Remote Control bridge's JWT expires and the subsequent OAuth token refresh returns 401, the bridge enters bridge_state: failed — and failed appears to be a terminal state with no retry. Manually toggling Remote Control off/on always recovers immediately, which proves the failure is recoverable — the app just never tries.

A likely trigger for the 401 itself: running multiple concurrent Claude Code sessions sharing the same OAuth credentials. At token expiry they race to refresh within seconds of each other; with refresh-token rotation, the loser is left holding a stale token and its refresh gets 401.

Environment

  • Claude Code CLI: 2.1.168 (desktop-bundled runtime: 2.1.170)
  • Claude desktop app (macOS): 1.11847.5
  • macOS 26.5 (build 25F71), Apple Silicon MacBook
  • Auth: claude.ai subscription OAuth (not API key)
  • 4+ concurrent Claude Code sessions typically running, several with Remote Control enabled

Evidence (from ~/Library/Logs/Claude/main.log)

Death sequence — multiple sessions racing to refresh at the same expiry boundary; most bridges win and reconnect, one loses and dies permanently:

2026-06-08 18:28:14 [info] [remote-control] bridge_state: reconnecting — JWT expired — refreshing
2026-06-08 18:28:14 [info] [oauth] CLI requested token refresh after 401
2026-06-08 18:28:21 [info] [remote-control] bridge_state: reconnecting — JWT expired — refreshing
2026-06-08 18:28:21 [info] [oauth] CLI requested token refresh after 401
2026-06-08 18:28:22 [info] [remote-control] bridge_state: connected
2026-06-08 18:28:24 [info] [remote-control] bridge_state: reconnecting — JWT expired — refreshing
2026-06-08 18:28:24 [info] [oauth] CLI requested token refresh after 401
2026-06-08 18:28:27 [info] [remote-control] bridge_state: connected
2026-06-08 18:28:28 [info] [remote-control] bridge_state: reconnecting — JWT expired — refreshing
2026-06-08 18:28:28 [info] [oauth] CLI requested token refresh after 401
2026-06-08 18:28:29 [info] [remote-control] bridge_state: reconnecting — JWT expired — refreshing
2026-06-08 18:28:29 [info] [oauth] CLI requested token refresh after 401
2026-06-08 18:28:30 [info] [remote-control] bridge_state: connected
2026-06-08 18:28:35 [info] [remote-control] bridge_state: failed — JWT refresh failed after 401

Second occurrence, two days later:

2026-06-10 10:23:35 [info] [remote-control] bridge_state: failed — Transport closed (code 401)

Aggregate bridge_state counts over the current log window:

138 bridge_state: connected
 23 bridge_state: ready
  6 bridge_state: reconnecting
  2 bridge_state: failed

After each failed there are zero subsequent reconnecting attempts for that bridge — the next state change only happens when Remote Control is manually re-enabled, which mints a fresh JWT and connects instantly.

Steps to reproduce

  1. Run several Claude Code sessions concurrently on one machine under the same claude.ai OAuth login (desktop app), with Remote Control enabled on them.
  2. Keep the machine awake and online; leave the sessions idle.
  3. Wait for an OAuth token expiry boundary (hours). All sessions attempt refresh within seconds of each other.
  4. Occasionally one bridge's refresh returns 401 → bridge_state: failed.
  5. Observe the session is no longer reachable from claude.ai web / mobile, indefinitely, until Remote Control is manually toggled.

Expected behavior

  • A 401 on JWT refresh should not be terminal. The bridge should retry with backoff, re-reading the (by then updated) shared credentials — the same credentials a manual re-enable uses successfully seconds later.
  • Concurrent sessions should coordinate token refresh (single-flight / file-lock on the shared credential store) so refresh-token rotation doesn't strand the race losers with stale tokens.
  • If the bridge does land in failed, surface it to the user (notification / badge) instead of failing silently — from the phone the session just looks gone.

Related (not duplicates)

  • #34255 — automatic reconnection never triggers after drops (generic; this report identifies a specific terminal-state path with the 401/JWT mechanism)
  • #31853 — periodic WebSocket disconnects, permanent after repeated reconnects
  • #65801 / #62924 — bridge_state: failed — /login at initial connect (auth webview); different failure point — here the bridge connects fine and dies later at token refresh

View original on GitHub ↗