[BUG] Remote Control bridge enters terminal "failed" state after JWT refresh 401 — never retries; concurrent-session token-refresh race triggers it
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
- Run several Claude Code sessions concurrently on one machine under the same claude.ai OAuth login (desktop app), with Remote Control enabled on them.
- Keep the machine awake and online; leave the sessions idle.
- Wait for an OAuth token expiry boundary (hours). All sessions attempt refresh within seconds of each other.
- Occasionally one bridge's refresh returns 401 →
bridge_state: failed. - 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 — /loginat initial connect (auth webview); different failure point — here the bridge connects fine and dies later at token refresh