`claude remote-control` server exits on 401 after exactly 24h — does not refresh its OAuth access token, killing every attached session
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
- Start
claude remote-control --name Xon a machine and leave it running. - Attach one or more sessions from the mobile/web client.
- 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).
- 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-control401 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
/bridgeendpoint after session creation. Different phase; cited above as evidence for the separate-RC-credential hypothesis.
Suggested fixes
- Refresh the OAuth access token proactively in the remote-control server, or on a 401 retry the request once after refreshing.
- Failing that, re-read
~/.claude/.credentials.jsonon 401 before giving up — in our case a valid token was sitting there. - 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.
3 Comments
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 asclaude --print --sdk-url ... --input-format stream-json, which never runs it.claude remote-control --helpexposes no--chromepassthrough.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
--chromeflag onclaude remote-control, passed to spawned sessions, would solve it properly.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-controlruns as a systemd service on a headless Ubuntu 26.04server, 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:
.credentials.jsonstill held a completeclaudeAiOauthblock, written 2026-08-28 19:52,with a
refreshTokenvalid until 2026-09-27. NoANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENanywhere (checked unit, EnvironmentFile and shell profiles), so #59062 does not apply,
and the block was not wiped as in #88487. Only
claude auth loginfixed 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 updateand 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=alwaysbrought the serviceback 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 checkthat looks at
systemctl list-units --state=failed.Your proposed fixes 2 and 3 (retry once after refreshing, re-read
.credentials.jsonbefore giving up) would cover the startup path as well as the poll path, if applied
there too.
Unrelated observation from the same file
.credentials.jsonhad accumulated 22mcpOAuthentries forClaude_Code_Remote, each with its ownclientIdand all 22 with an emptyaccessToken, emptyrefreshTokenand noexpiresAt- the "tokenless credentialstub" 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.
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-controlworker, all came up logged in. We added one thing after the 08-29 incident: the nightly job runsclaude auth status --jsonbefore 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.jsonwas rewritten at that exact second withexpiresAteight 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-controlas a systemd service.