`claude remote-control` server exits on 401 after exactly 24h — does not refresh its OAuth access token, killing every attached session

Status Open
Reported on v2.1.233
Maintainer reply None cached
Activity 3 comments · opened Aug 19, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report
  • [x] I am using the latest version of Claude Code

Version: 2.1.233 (npm global) · macOS 15 (Darwin 25.6.0) · Apple Silicon Mac mini · Max subscription

Summary

A long-running claude remote-control server dies exactly 24 hours after the process starts, with a 401 "OAuth access token has expired". It does not attempt a refresh, and it does not re-read the refreshed credentials that are already on disk. Every Remote Control session attached to that server is orphaned the instant it exits — the phone/web client shows the environment as gone, and in-flight work (running subagents, uncommitted edits) is lost.

The 24h figure is not approximate. Two independently-started servers on the same machine:

| Server | Started | Died | Lifetime |
|---|---|---|---|
| rc-claude | Aug 18 13:23:03 | Aug 19 13:23:09 | 24h 00m 06s |
| rc-hardware / rc-software / rc-workflows | Aug 18 13:01:51 | Aug 19 13:01:53 | 24h 00m 02s |

The second row is three servers dying in the same second, having been launched in the same second.

Exact error

Captured from the tmux pane at the moment of exit (process exit status 0):

[13:23:09] Error: Poll: Authentication failed (401): OAuth access token has expired.
Re-authenticate to continue.. Remote Control is only available with claude.ai
subscriptions. Please use `/login` to sign in with your claude.ai account.

Note the doubled period in continue.. — likely two message strings concatenated.

The credentials on disk were valid

This is the key point: the machine's auth was fine. At the moment the server died holding an "expired" token, ~/.claude/.credentials.json contained:

claudeAiOauth.expiresAt            = 2026-08-19 19:30:38   (6h in the future)
claudeAiOauth.refreshTokenExpiresAt = 2026-09-14 06:51:40   (26 days in the future)
file mtime                          = 2026-08-19 11:30:38   (refreshed 2h earlier)

So another claude process had already refreshed the token successfully. The remote-control server appears to hold the access token it obtained at startup in memory for its whole life, never refreshing it and never re-reading the file. /login is not the fix — restarting the process is, which is why "run claude again" appeared to fix earlier incidents.

Expected

The server refreshes its access token before expiry (the refresh token is valid for weeks) and keeps serving. A 401 on a poll should trigger a refresh-and-retry, not process exit.

Actual

Process exits with status 0. Every attached session is orphaned with no warning, at a time of day determined by whenever the server last happened to start.

Impact

The exit time inherits the server's start time, so the failure lands mid-workday for anyone whose server was last restarted mid-workday. In our case it landed at 13:01 on two consecutive days, twice killing active sessions with running subagents. Because it presents as "the environment no longer exists" on the client, it reads as a network or account problem — we chased a network-outage theory and a watchdog theory for two days before catching the actual error, since the pane is erased on exit.

Reproduce

  1. Start claude remote-control --name X on a machine and leave it running.
  2. Attach one or more sessions from the mobile/web client.
  3. Wait 24 hours (no interaction needed; it dies whether idle or busy — our idle server and our 3-session server both died at their own 24h marks).
  4. The server exits with the 401 above; attached sessions are unrecoverable in place.

Why I think this is distinct from the RC credential path already reported

#78453 shows the RC client fetching remote credentials from a /bridge endpoint at session-creation time — i.e. Remote Control mints and holds a credential of its own rather than using the account access token directly. That fits what I see: my account access token rotates on a several-hour cycle (the file showed an 8h validity window and had been refreshed 2h before the crash), yet the server survived a clean 24h from process start, twice. So the expiring thing looks like an RC-specific credential obtained once at startup and never renewed for the life of the process.

Related issues

Same root class (token refresh in long-lived processes), but none covers a remote-control server exiting at its own 24h mark:

  • #87271 — desktop app: OAuth refresh is reactive-only, surfacing on every ~2h rotation. Same underlying "refresh is not proactive" theme, different surface and different interval.
  • #82217 — OAuth expiration not propagated to active subagent requests; long-running sessions "all broke overnight" with simultaneous 401s. Closest in spirit; that report is about subagent requests inside a session, not the RC server process exiting.
  • #83958 — claude remote-control 401 at the Registration phase with "Missing Authorization header". Different phase, different message, and it fails at startup rather than after 24h of healthy service.
  • #78453 — 401 at the /bridge endpoint after session creation. Different phase; cited above as evidence for the separate-RC-credential hypothesis.

Suggested fixes

  1. Refresh the OAuth access token proactively in the remote-control server, or on a 401 retry the request once after refreshing.
  2. Failing that, re-read ~/.claude/.credentials.json on 401 before giving up — in our case a valid token was sitting there.
  3. Do not exit 0 on a fatal auth error, and write the reason to stderr as well as the TUI. The alternate-screen restore wipes the message on exit, which is what made this undiagnosable for two days.

View original on GitHub ↗

3 Comments

orlando-ts · 1 month ago

Related gap in the same area: no way to enable Claude in Chrome for remote-control sessions.

Chasing the above, I found that RC-spawned sessions also lack the Claude in Chrome tools. The cause is the same shape: Claude in Chrome is registered dynamically on the interactive startup path as a stdio MCP server (claude --claude-in-chrome-mcp), and RC spawns sessions as claude --print --sdk-url ... --input-format stream-json, which never runs it. claude remote-control --help exposes no --chrome passthrough.

It isn't a sandbox problem — the bridge socket under /tmp/claude-mcp-browser-bridge-<user>/ is reachable from an RC session, and registering the identical server by hand (claude mcp add) gives a working set of all 22 tools, verified driving a real browser from a headless session.

But the workaround lands outside the permission model, because that model keys off the reserved server name: under any other name the tools are anonymous MCP tools, so the per-domain rules aren't reachable. That leaves only two states — prompt on every call, or CLAUDE_CHROME_PERMISSION_MODE=skip_all_permission_checks. Neither is right for an unattended remote session, and the middle ground the built-in path has isn't available.

A --chrome flag on claude remote-control, passed to spawned sessions, would solve it properly.

Roorn · 1 month ago

Linux data point from the other end of the same problem: the refresh gap also bites
on restart, not just after 24h uptime.

Setup: claude remote-control runs as a systemd service on a headless Ubuntu 26.04
server, restarted by a nightly job whenever the CLI updates. Because of that nightly
restart the server never reaches the 24h mark you describe, so I cannot confirm the
24h exit itself. What I can add is what happens when the restart lands near the token
boundary.

What happened

On 2026-08-29 at 03:33:14 the CLI updated 2.1.250 -> 2.1.251 and the service was
restarted in the same second. Every start from that moment failed with:

Error: You must be logged in to use Remote Control.
Remote Control is only available with claude.ai subscriptions. Please use `/login` to sign in with your claude.ai account.

.credentials.json still held a complete claudeAiOauth block, written 2026-08-28 19:52,
with a refreshToken valid until 2026-09-27. No ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
anywhere (checked unit, EnvironmentFile and shell profiles), so #59062 does not apply,
and the block was not wiped as in #88487. Only claude auth login fixed it.

Why I think it is the same area

Fresh tokens here are valid for 8 hours. The stored token was written 19:52, so it
expired around 03:52 - the restart at 03:33:14 fell 19 minutes before expiry, inside
the window where a client would normally refresh. The 2.1.248 entry
*"Fixed being sent to the login screen when another Claude Code process held the token
refresh lock while the session token had expired"* describes exactly this shape, and
claude update and the starting server were touching credentials in the same second.
I cannot prove it: the old token's expiry is gone, overwritten by the re-login.

The measurement that makes it interesting

| | |
|---|---|
| CLI updates since 2026-08-04 (each followed by a restart) | 20 |
| Nightly restarts of the service in August | 42 |
| Nights with a login failure | 1 |
| Login failures before 03:33:14 that night | 0 |

So 19 identical updates were fine. This is not the restart policy being wrong; it is
a narrow race that needs the restart to coincide with the refresh window.

What made it expensive

There is no retry and no backoff on this path. Restart=always brought the service
back 175 times over five hours on an error that can never resolve itself, and because
it kept restarting it never settled in failed, so it stayed invisible to any check
that looks at systemctl list-units --state=failed.

Your proposed fixes 2 and 3 (retry once after refreshing, re-read .credentials.json
before giving up) would cover the startup path as well as the poll path, if applied
there too.

Unrelated observation from the same file

.credentials.json had accumulated 22 mcpOAuth entries for
Claude_Code_Remote, each with its own clientId and all 22 with an empty
accessToken, empty refreshToken and no expiresAt - the "tokenless credential
stub" of #87405. Three distinct field shapes across them, so they built up over
months rather than during this incident. Removing all 22 left login working. Flagging
it here because it is the same file and the same subsystem; happy to move it to
#87405 if that fits better.

Versions: CLI 2.1.251 (native install), Ubuntu 26.04.1, headless, systemd service.

Roorn · 1 month ago

Ten days of follow-up on the restart variant from my 2026-08-29 comment, plus one observation that may narrow it down.

No recurrence. Six CLI updates since then (2026-09-01, 02, 03, 04, 05 and 07), each with a restart of the remote-control worker, all came up logged in. We added one thing after the 08-29 incident: the nightly job runs claude auth status --json before restarting, and skips the restart when that says "not logged in". So far it has never had to skip.

Uptime beyond 24 hours is fine here. Between the updates of 09-05 and 09-07 the worker ran for 48 hours on 2.1.261 without losing its login, so the 24-hour exit is not universal.

The on-disk token is refreshed by short-lived CLI runs. This morning (2026-09-08 03:34:07) the nightly job only ran claude update, which reported "up to date", so nothing was restarted. .credentials.json was rewritten at that exact second with expiresAt eight hours later. The long-running worker had been up since 09-07 03:41. Whether the worker reads that refreshed file or refreshes on its own we cannot tell from the outside, but it does mean a daily short CLI invocation keeps the file fresh, which may be why our restarts go well and the 08-29 failure only hit when the restart landed 19 minutes before expiry.

Setup unchanged: Ubuntu Server 26.04, CLI 2.1.263, claude remote-control as a systemd service.