Remote Control (Windows desktop): disconnect crash permanently sticks the toggle; all sessions now fail with "Remote credentials fetch failed"

Status Fixed / completed
Reported on v2.1.209
Maintainer reply None cached
Activity 3 comments · opened Jul 17, 2026 · closed Aug 25, 2026

Summary

I drive my Windows desktop Claude Code sessions from an iPhone via Remote Control. Two defects make this unusable:

  1. A disconnect crash leaves the toggle permanently stuck, so a dropped session can never be re-armed — only a full app restart clears it.
  2. Since 2026-07-17 17:45 JST, Remote Control fails account-wide with Remote credentials fetch failed, for every session including brand-new forks. This survived an app restart and an app update.

Everything below is copied from logs on my machine, not inference.

Environment

| | |
|---|---|
| App | Claude desktop, Windows. Reproduced on 1.21459.3.0, still failing after updating to v1.22209.0 |
| Claude Code | bundled 2.1.209; CLI on PATH 2.1.212 (auto-updating) |
| Entrypoint | CLAUDE_CODE_ENTRYPOINT=claude-desktop |
| OS | Windows 11 Pro 10.0.26200 |
| Plan | Claude Max (personal account with an auto-generated Organization) |
| Mobile | Claude iOS app |

(My orgId is available on request — omitting it here since this is a public tracker.)

---

Defect 1 — Disconnect crash leaves Remote Control permanently stuck

Observed in-session error, reproduced repeatedly:

Remote Control failed to disconnect: Cannot read properties of undefined (reading 'session_url')

Behavior: when the bridge has silently died but the session still believes it is connected, /remote-control (a toggle) attempts to disconnect first. That disconnect dereferences a session object that no longer exists and throws. The state is never cleared, so the toggle can never be turned back on. Running /remote-control again produces the identical error, indefinitely.

Consequence: the only recovery is restarting the whole app. (I had been restarting the entire PC, because re-running the command visibly does nothing but error.)

Expected: disconnect should be defensive — if the session object is already gone, treat it as already-disconnected and allow re-arming.

---

Defect 2 — Account-wide Remote credentials fetch failed (current blocker)

From %APPDATA%\Roaming\Claude\logs\main.log:

2026-07-17 17:45:52 [info] Enabling remote control for session local_ae0865eb-…
2026-07-17 17:45:57 [info] [remote-control] bridge_state: failed — Remote credentials fetch failed — see debug log
2026-07-17 17:45:57 [error] Failed to toggle remote control for local_ae0865eb-…: Remote credentials fetch failed — see debug log

2026-07-17 17:46:11 [info] Enabling remote control for session local_b97b6146-…
2026-07-17 17:46:16 [info] [remote-control] bridge_state: failed — Remote credentials fetch failed — see debug log

2026-07-17 19:47:09 [info] Enabling remote control for session local_ae0865eb-…
2026-07-17 19:47:12 [info] [remote-control] bridge_state: failed — Session creation failed — see debug log

2026-07-17 19:47:30 [info] Enabling remote control for session local_70172bf2-…
2026-07-17 19:47:35 [info] [remote-control] bridge_state: failed — Remote credentials fetch failed — see debug log

local_70172bf2-… was a freshly forked session. It failed identically — so this is not per-session state; the failure is upstream of the session.

The client-side OAuth token looks healthy at the same time:

2026-07-17 19:47:49 [info] [oauth-v2] lookup orgId=<redacted>
    cacheKey=…:https://api.anthropic.com:user:inference user:file_upload user:profile user:sessions:claude_code
2026-07-17 19:47:49 [info] [oauth-v2] using cached token for orgId=<redacted>

No 401/403, no expiry, and the user:sessions:claude_code scope is present. Inference itself works fine throughout. So the failure appears specific to whatever credential the Remote Control bridge fetches.

Two distinct failure strings appear: Remote credentials fetch failed and Session creation failed.

Sub-issue: the error tells me to read a debug log that I cannot find

Both messages end with "see debug log", but main.log only contains the summary line — no underlying cause, HTTP status, or request id. There is no discoverable debug log with that detail. Please either surface that log or include the real cause in the message; as written, the instruction is a dead end for users.

---

Defect 3 — Renderer memory growth

From the app's own periodic self-report in main.log:

2026-07-17 17:45:47 [process-memory] trigger=interval tree_rss_sum=13535MB electron(14)=13535MB
  top=[electron_renderer:19140:10575MB electron_renderer:5284:912MB electron_main:17648:512MB …]

A single renderer reached 10,575 MB after roughly 18 hours of uptime; the app tree totalled 13.5 GB.

After restarting the app:

2026-07-17 19:50:54 [info] [process-memory] trigger=interval tree_rss_sum=2222MB electron(12)=2222MB
  top=[renderer:5184:687MB electron_main:19968:342MB …]

13.5 GB → 2.2 GB, largest renderer 680 MB. About 15 GB of system RAM was reclaimed.

Possibly relevant: my heaviest session's transcript on disk is 246 MB (~/.claude/projects/<project>/<session>.jsonl). That session involved many pasted screenshots.

---

What I have already ruled out

  • Not the machine sleeping. Sleep and hibernate are fully disabled — standby-timeout and hibernate-timeout are 0x00000000 on both AC and DC (verified via powercfg /q SCHEME_CURRENT SUB_SLEEP).
  • Not CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC. It is not set.
  • Not stale/expired auth. oauth-v2 reports using cached token with the correct scopes, and inference works.
  • Not per-session state. A brand-new forked session fails identically.
  • Not fixed by restarting. Restarting the app cleared Defect 1 and the memory bloat, but Remote credentials fetch failed persists.
  • Not fixed by updating. Updating to v1.22209.0 did not help.

Impact

My primary use case is operating my desktop sessions from an iPhone while away from the machine. Two things make this fragile by design:

  1. Recovery requires physical access to the host. Re-arming is only possible by typing /remote-control on the PC — the mobile client cannot re-arm. So a drop while I'm out ends that session for the rest of the day.
  2. Right now nothing can be armed at all, so Remote Control is completely unusable on this account.

I have moved to the Discord channel bridge (--channels plugin:discord@claude-plugins-official) as a fallback, since it does not depend on the Remote Control service. That works, but it only covers a terminal session — not the desktop app sessions I actually work in.

Requests

  1. Make disconnect defensive so the toggle cannot get permanently stuck (Defect 1). If the session object is already gone, treat it as disconnected rather than throwing.
  2. Investigate Remote credentials fetch failed / Session creation failed for my account (orgId available on request). The client token is healthy, so this appears to be server-side.
  3. Surface the "debug log" the error refers to, or put the actual cause (status code / request id) into the message.
  4. Provide a supported way to re-arm from the mobile client, or an auto-reconnect that actually recovers. Requiring physical access defeats the purpose of the feature.
  5. Investigate renderer memory growth (Defect 3) — 10.5 GB in a single renderer looks like a leak, plausibly related to image-heavy transcripts.

Happy to provide

Full main.log, claude doctor output, my orgId, or any additional diagnostics on request.

View original on GitHub ↗

3 Comments

sgg-cyber · 1 month ago

Reproduced Defect 1 on Windows with app 1.24012.9.0 / bundled CLI 2.1.219, and I think I can point at the exact lines. Everything quoted below is from resources/app.asar and the bundled claude.exe on my machine.

1. The dereference that throws

In the session-manager's remote-control toggle:

const c = await e.query.enableRemoteControl(n, n ? e.title : void 0),
      l = (a = c.session_url) == null ? void 0 : a.split("/").filter(Boolean).pop();

The optional chaining is on the value (c.session_url?.split(...)), not on c itself. If c is undefined, this throws Cannot read properties of undefined (reading 'session_url') — the exact string in this issue.

enableRemoteControl is a thin wrapper that returns whatever the control response carries:

async enableRemoteControl(t, r) {
  return (await this.request({subtype:"remote_control", enabled:t, ...(r!==void 0&&{name:r})})).response
}

And in the CLI's remote_control control-request handler, the two branches are asymmetric — the enable branch answers with a payload, the disable branch answers with none:

// enable
Mn(dt, {session_url: ew(kr.bridgeSessionId, kr.sessionIngressUrl),
        connect_url: Gmn(kr.environmentId, kr.sessionIngressUrl),
        environment_id: kr.environmentId})
// disable
} else {
  if (z) { …await z.teardown({reason:"remote_control_disabled"}); z = null; … }
  Mn(dt)          // <-- no payload
}

(I did not decompile Mn itself — it is a reused minified name — so I can't say from source alone whether it emits response: undefined or an empty object. But the observed exception proves c is undefined on this path, and the missing guard on c is plain in the source.)

2. Why it sticks permanently — this is the part that makes it unrecoverable

The catch block only clears the state on the enable path:

catch (c) {
  const l = c instanceof Error ? c.message : String(c);
  o.logger.error(`Failed to toggle remote control for ${t}: ${l}`),
  n ? (e.remoteControlEnabled = !1, e.remoteControlAutoEnabled = void 0,
       e.bridgeSessionId = void 0, e.bridgeSessionUrl = void 0,
       …`Remote Control failed to connect: ${l}`…)
    : (…`Remote Control failed to disconnect: ${l}`…)   // <-- no state reset
  …
}

and the desired direction is derived from that same flag:

const n = (r == null ? void 0 : r.desired) ?? !e.remoteControlEnabled;

So once a bridge dies silently while remoteControlEnabled is still true: /remote-controln = false → disable path → throw → state not reset → next /remote-controln = false again → identical throw, forever. An app restart is the only thing that clears the in-memory flag, exactly as reported.

A guard on c would stop the exception, but the asymmetric catch is worth fixing independently — any failure on the disable path currently leaves the toggle wedged.

3. What puts a session into this state: lingering background tasks

On my machine the sessions that lose Remote Control are consistently the ones with a background task still registered, and pauseSession() explains why. It early-returns for isRunning, activeCronJobs, pendingLoopWakeup and background tasks:

o.logger.debug(`[CCD] Skipping pause for session ${e} - query is actively running`)
o.logger.debug(`[CCD] Skipping pause for session ${e} - ${t.activeCronJobs.size} active cron job(s)`)

The affected session logged this every 15 minutes for more than five hours straight:

2026-07-30 16:56:51 [info] [CCD] Skipping pause for session local_4e64423b-… - 1 active background task(s)
2026-07-30 17:11:51 [info] [CCD] Skipping pause for session local_4e64423b-… - 1 active background task(s)
…
2026-07-30 21:56:51 [info] [CCD] Skipping pause for session local_4e64423b-… - 1 active background task(s)
2026-07-30 22:05:10 [info] Disabling remote control for session local_4e64423b-…
2026-07-30 22:05:12 [error] Failed to toggle remote control for local_4e64423b-…: Cannot read properties of undefined (reading 'session_url')

Sessions on the same machine without a background task were paused normally and kept working — one of them armed cleanly 20 seconds later:

2026-07-30 22:05:31 [info] Enabling remote control for session local_4e225838-…
2026-07-30 22:05:32 [info] [remote-control] bridge_state: connected

So a background task pins the session unpaused indefinitely; its bridge eventually dies on its own (in my earlier logs, usually Transport closed: … no longer the active worker for the session (code 4090), which has no reconnect path — only 401 and 4091 are retried in setOnClose); and from then on Defect 1 makes it unrecoverable without restarting the app.

Workaround for anyone hitting this: don't leave long-running background tasks in a session you need to keep remote-controlling, and re-arm Remote Control after such a task finishes. Once the error appears, restarting the app is the only recovery.

sgg-cyber · 1 month ago

Addendum: proposed fix, scoped so it can be applied as-is.

Users cannot patch this themselves — the defective code lives in the desktop app's app.asar inside the signed MSIX package (C:\Program Files\WindowsApps\…, TrustedInstaller-owned, signature-validated) — so here is the smallest complete fix for the maintainers. Everything is in the one session-manager method quoted in my previous comment; the CLI needs no change (its disable branch already tears down the bridge and nulls its own state before responding, so after the app-side throw the CLI is clean — only the app's in-memory flags are wedged).

Names below are the minified ones from app.asar in app build 1.24012.9.0; e = session record, n = desired direction, c = control response.

Change 1 — don't dereference a payload that the disable path legitimately doesn't send.

// before
const c = await e.query.enableRemoteControl(n, n ? e.title : void 0),
      l = (a = c.session_url) == null ? void 0 : a.split("/").filter(Boolean).pop();

// after
const c = await e.query.enableRemoteControl(n, n ? e.title : void 0),
      l = (a = c?.session_url) == null ? void 0 : a.split("/").filter(Boolean).pop();

The disable branch of the CLI's remote_control handler answers with no payload (Mn(dt) vs the enable branch's Mn(dt, {session_url, connect_url, environment_id})), so c === undefined is a normal disable outcome, not an error. Every downstream use in this method already tolerates undefined (l is guarded, and the disable arm of the success path only writes e.remoteControlEnabled = !1 etc.). Alternatively — or additionally, for defense in depth — the CLI could answer Mn(dt, {}) on the disable branch; either side of the contract fixed alone stops the crash, but the app-side guard also covers older CLI versions.

Change 2 — make the catch reset state on the disable path too, so no failure can wedge the toggle.

// before
: i || this.emitSyntheticAssistantMessage(e, `Remote Control failed to disconnect: ${l}`, e.isRunning ? {skipResult: !0} : void 0),

// after
: (e.remoteControlEnabled = !1, e.remoteControlAutoEnabled = void 0,
   e.bridgeSessionId = void 0, e.bridgeSessionUrl = void 0,
   i || this.emitSyntheticAssistantMessage(e, `Remote Control failed to disconnect: ${l}`, e.isRunning ? {skipResult: !0} : void 0)),

Rationale: by the time the disable path can throw, enableRemoteControl(false) has either already run (CLI tore the bridge down) or failed against a bridge that is already dead — in both cases the truthful app state is "not connected". Clearing the flags means the next /remote-control computes n = !e.remoteControlEnabled = true and takes the enable path, i.e. the toggle recovers instead of looping on disconnect forever. This is the part that turns "restart the whole app" into "just toggle again".

Change 1 alone stops the exception; change 2 alone stops any future disable-path exception from wedging the toggle permanently. Both are one-line-scale and confined to this method.

Left out deliberately (separate defects, separate fixes): the bridge's missing reconnect for close code 4090 (only 401/4091 reconnect in setOnClose), and the background-task pause-skip that keeps such sessions alive long enough to hit this — happy to file those separately if useful.

f5022f-rgb · 1 month ago

Same here, and still reproducing on the latest versions (2026-07-31 JST).

Environment
OS Windows 11 Home, build 10.0.26200 (same build as the original report)
Claude Code (desktop app) 2.1.219
Claude Code (CLI) 2.1.220 (updated manually for this test)
Plan Claude Max (personal)
Auth Claude account / OAuth (no API key)
Symptom
Toggling Remote Control on always fails with:

Remote Control を切り替えられませんでした。もう一度お試しください。 (Couldn't toggle Remote Control. Please try again.)

Notably, the UI looks like it partially succeeded:

the toggle turns blue (ON)
a session URL is shown under "Open in browser" (e.g. https://claude.ai/code/session_01RHUJ…)
that URL actually opens in a browser
…but the toggle still reports failure, and Remote Control never works.

What I ruled out (all clean)
Plan: Max (supported)
Auth: OAuth via Claude account, ANTHROPIC_API_KEY not set
Env vars: DISABLE_TELEMETRY, DO_NOT_TRACK, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_GROWTHBOOK, ANTHROPIC_BASE_URL — none set in User or Machine scope
Managed policy: C:\ProgramData\ClaudeCode\managed-settings.json does not exist
Network: claude.ai, api.anthropic.com, statsigapi.net, featureassets.org all reachable on 443
Telemetry: no new failed-event records around the failures (client-side sending works)
claude doctor reports "No installation issues found."
Also tried (no effect)
Retrying the toggle many times
Opening the generated session URL (it opens fine)
Sign out → sign in
Restarting the app
Updating the CLI 2.1.207 → 2.1.220
Rebooting the PC
Starting a brand-new session
Extra data point: there is no CLI fallback
The docs mention /config → "Enable Remote Control for all sessions", but I could not reach that toggle from a CLI session:

Inside the desktop app's terminal panel, /config replies that the settings panel lives in the app UI, so it cannot be opened from there.
In a standalone Claude Code terminal window, typing /config was interpreted as a request and offered a settings-file skill instead of opening the Remote Control toggle.
So the only way to enable Remote Control appears to be the desktop app toggle — which always fails. There is no workaround path for desktop-app sessions.

Minor inconsistency (may or may not be related)
The session header shows Claude Max, but the local telemetry payload records "subscription_type":"pro". The two disagree inside the same client.

Error message discoverability
Same problem as reported above: the error says to retry, but nothing surfaces an HTTP status or request ID, so there is nothing actionable on the client side to diagnose further.