Subscription-OAuth long-running Remote Control session dead-ends at 401 / "Please run /login" at the daily refresh boundary; /login cannot complete in a remote-controlled session

Open 💬 0 comments Opened Jul 2, 2026 by magicwave

Subscription-OAuth long-running Remote Control session dead-ends at 401 / "Please run /login" at the ~daily token-refresh boundary; /login cannot complete in a remote-controlled session

Environment

  • Platform: macOS (darwin 25.5.0)
  • Claude Code: 2.1.198 (native installer). Note the failing process had been running continuously for ~27h and was still executing the in-memory 2.1.197 build (a native auto-update repointed the on-disk binary to 2.1.198 mid-session, without the running process reloading — see "Secondary observation").
  • Auth: Claude subscription OAuth login, credentials in the macOS Keychain (Claude Code-credentials). No ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_AUTH_TOKEN set; no ~/.claude/.credentials.json.
  • Usage: one long-running (24h+) session ("captures") driven from the Claude iPhone app Remote Control feature, running concurrently with other local claude CLI sessions and the phone app — all three sharing the one Keychain OAuth credential.

Summary

A long-running session that is being driven via iPhone Remote Control periodically dies with:

⏺ Please run /login · API Error: 401 Invalid authentication credentials

This recurs roughly every ~24 hours, i.e. at the access-token-expiry / refresh boundary. It appears to be the subscription-OAuth sibling of a bug class that has been fixed many times before (see "Related prior fixes"), and specifically the sibling of the AWS/Mantle STS fix shipped in 2.1.198. The usage pattern (long remote session + concurrent local sessions + phone app) is unchanged from months of prior trouble-free use, which points to either a recent regression or a server-side token-policy change (shorter TTL / stricter single-use rotation) re-exposing the fundamental multi-client shared-credential race.

Two distinct problems

1) The 401 itself (root cause: multi-client refresh-token race)

The captures session, concurrent local CLI sessions, and the phone app share one Keychain OAuth credential with single-use rotating refresh tokens. When a concurrent client refreshes, the server invalidates this session's refresh token; when the long session later crosses its access-token expiry and tries to reactively refresh, its refresh token is already spent → 401 → "Please run /login".

2) /login cannot recover a remote-controlled session in place

When the user runs /login inside the remote-controlled session:

  1. Browser opens, user authorizes, browser shows the "You're all set up for Claude Code / You can now close this window" success page.
  2. The CLI never receives the OAuth loopback callback and stays at Login OAuth error: Press Enter to retry.
  3. Pressing Enter re-opens the browser (skips re-auth, straight to Authorize→success) but the CLI never advances.

The interactive loopback callback does not return through the Remote Control channel, so in-place /login can never complete. Historically the only "fix" was quitting and starting a brand-new local session.

Workaround that works

Pressing ESC to cancel the stuck /login prompt and then retrying the request lets the session resume — i.e. the built-in reactive-refresh-on-401 (fixed in 2.1.117) succeeds once the shared credential has been refreshed out of band. So the interactive /login is both doomed and unnecessary; ESC + retry is the correct recovery, but this is non-obvious and the UI actively steers the user toward the doomed /login.

Expected behavior

  • A long-running subscription-OAuth session should reactively refresh at 401 and recover without surfacing "Please run /login", even when other clients (phone app, concurrent CLI sessions) share the credential — analogous to the awsAuthRefresh fix for the AWS/Mantle path in 2.1.198.
  • If re-login is genuinely required, it must be completable from within a Remote-Control-driven session, or the CLI should clearly tell the user to ESC + retry / start a fresh local session rather than hang at "Login OAuth error".

Related prior fixes (all already present in the running build)

  • 2.1.117 — Plain-CLI OAuth sessions dying with "Please run /login" when the access token expires mid-session; now refreshed reactively on 401.
  • 2.1.118 — macOS keychain race where a concurrent token refresh overwrites a fresh OAuth token, causing unexpected "Please run /login"; OAuth refresh failing when the server revokes a token before local expiry.
  • 2.1.126 — Concurrent credential write clearing a valid OAuth refresh token.
  • 2.1.133 — Parallel sessions all dead-ending at 401 after a refresh-token race wiped shared credentials.
  • 2.1.136 — Login loop where a concurrent write overwrites a freshly-rotated OAuth token and forces re-login.
  • 2.1.169 — Remote Control stuck reconnecting when an OAuth refresh happens concurrently.
  • 2.1.178 — Model requests still failing after credentials were refreshed outside the session (stale cached request config).
  • 2.1.198 — AWS/Mantle sessions dead-ending at "Please run /login" when the STS token expires (the sibling of this bug, on a different auth backend).

Related open feature requests: #36807 (Remote Control sessions should persist without interactive re-login) and #50977 (support setup-token/API-key auth for Remote Control) — relevant because a static CLAUDE_CODE_OAUTH_TOKEN would immunize against the race but currently disables Remote Control (#33105, #59062), leaving no configuration that closes the race while keeping phone control.

Secondary observation (version skew under long sessions)

With autoUpdates: false set in ~/.claude.json, the native installer still auto-updated (2.1.197 → 2.1.198) mid-session and repointed the claude symlink, while the long-running process kept executing 2.1.197 in memory for ~20h. Long-running sessions should either honor autoUpdates: false for the native updater as well, or make the in-session version skew visible so users know to restart after an update lands.

View original on GitHub ↗