WSL remote: CLI download livelocks on slow connections (180s idle timeout, no fetch progress, concurrent installs delete each other's temp files)

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 28, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Opening a WSL project from Claude Desktop (Windows, MSIX build) hangs on "Starting session" and fails every time with:

SSH connection setup timed out during "deploying: Downloading Claude Code CLI..." (idle 180003ms)

The remote server binary (claude-ssh, version 7d193f89fc02cf1035a391245312e34ad419f63e) uploads fine via SFTP. The failure is in the next step, where the remote server fetches the Claude Code CLI .zst (https://downloads.claude.ai/claude-code-releases/2.1.246/linux-x64/claude.zst, 78,105,392 bytes) into ~/.claude/remote/ccd-cli/. Three interacting behaviours make this step impossible to complete on a slow link:

  1. The remote fetch reports no progress to the client. Server uploads emit SFTP upload progress lines every ~1MB, but during --install --cli-url ... nothing is emitted until the download finishes. The client's last-progress marker stays at the server upload (last progress: 7127224/7127224B).
  1. The client aborts after 180s of "idle". Because (1) produces no output, any CLI download needing more than 180s is killed, even though it is progressing normally.
  1. Concurrent installs delete each other's .fetch-* temp files. The client runs overlapping setups (trigger: warm_up and trigger: start_session start within the same second), and aborted installs leave orphaned fetches running in WSL. The install path's prune/sweep (install.pruneCliDir / install.sweepOrphans) removes the other run's in-progress temp file, so even a fetch that finishes under 180s dies:

[BinaryDeployment] Remote CLI fetch failed, falling back to SFTP: chmod: chmod /home/ayman/.claude/remote/ccd-cli/.fetch-1089879865: no such file or directory

The whole ~/.claude/remote/ tree is also wiped between attempts (remote=missing on the next setup), so the server is re-uploaded and the cycle restarts from zero.

Measured numbers

| Measurement | Value |
|---|---|
| CLI artifact size (claude-code-releases/2.1.246/linux-x64/claude.zst) | 78,105,392 bytes |
| Sustained download speed, WSL → downloads.claude.ai | ~284 KB/s |
| Sustained download speed, Windows host → downloads.claude.ai (same file, full 78MB) | ~313 KB/s |
| Time required at 284 KB/s | ~275 s |
| Client idle timeout | 180 s |
| Minimum speed needed to finish inside the timeout | ~450 KB/s (78 MB / 180 s) |

The WSL remote path is effectively unusable for anyone whose connection sustains less than ~450 KB/s to downloads.claude.ai. A fixed wall-clock timeout on a ~78MB download is the wrong tool — the timeout should apply to stalled transfers, which requires progress reporting from the remote fetch.

What Should Happen?

Opening a WSL project should start a session. The CLI download step should either complete or fail with a clear, actionable error.

Specifically:

  • The remote fetch should stream progress (bytes received / total) the way SFTP uploads already do.
  • The idle timeout should measure stalled transfers, not wall-clock time since the last message. A download progressing normally at 284 KB/s should not be killed.
  • Concurrent setups (warm_up + start_session) should be serialized per target, and an aborted install should terminate its remote fetch rather than leaving it orphaned.
  • pruneCliDir / sweepOrphans should not delete .fetch-* files belonging to an in-flight download.
  • Ideally, the remote server should reuse a compatible CLI already installed on the machine (e.g. ~/.local/share/claude/versions/<version>) when the version matches.

Error Messages/Logs

From `%LOCALAPPDATA%\Claude\Logs\ssh.log`:

15:36:57 [info] [BinaryDeployment] Deploying server binary linux-amd64 to /home/ayman/.claude/remote/srv/7d193f89... (remote=missing, ...)
15:36:59 [info] [BinaryDeployment] SFTP upload progress: .../server 7127224/7127224B (5.41 MB/s, 1256ms elapsed)
15:40:00 [warn] [BinaryDeployment] Aborting setup on wsl:Ubuntu
15:40:00 [error] [RemoteServerController] Connection failed (183900ms, trigger: warm_up, attempt 1): SSH connection setup timed out during "deploying: Downloading Claude Code CLI..." (idle 180003ms; last progress: 7127224/7127224B)
15:40:00 [info] [RemoteServerController] Ensuring server is ready on wsl:Ubuntu (trigger: start_session)
15:40:00 [warn] [BinaryDeployment] server --install produced no result; stderr (no detail)
15:43:00 [error] [RemoteServerController] Connection failed (180544ms, trigger: start_session, attempt 1): SSH connection setup timed out during "deploying: Downloading Claude Code CLI..." (idle 180002ms)
15:48:13 [warn] [BinaryDeployment] Remote CLI fetch failed, falling back to SFTP: chmod: chmod /home/ayman/.claude/remote/ccd-cli/.fetch-1089879865: no such file or directory

The same abort/retry pair repeated ~10 times over 90 minutes with identical timings.

Running the install step manually returns success with an empty result, which is the expected first-run answer (the pinned version simply isn't in the managed dir yet):

$ ~/.claude/remote/srv/*/server --install
__INSTALL_RESULT__{"serverVersion":"7d193f89...","os":"linux","arch":"amd64","libc":"glibc","cliPath":"","cliWasPresent":false}
exit code: 0

Steps to Reproduce

  1. Windows 11 + WSL2 (Ubuntu), on a connection that sustains under ~450 KB/s to downloads.claude.ai.
  2. Open Claude Desktop and add a project folder located inside WSL (e.g. /home/<user>/projects/<project>).
  3. Open that project. The app shows "Starting session".
  4. After ~180s it fails with: SSH connection setup timed out during "deploying: Downloading Claude Code CLI..." (idle 180003ms).
  5. Retries repeat indefinitely with identical timing. ~/.claude/remote/ is wiped between attempts, so each cycle restarts from zero.

To confirm the cause, measure throughput to the artifact from inside WSL:

curl -o /dev/null -w 'avg %{speed_download} B/s, total %{time_total}s\n' \
https://downloads.claude.ai/claude-code-releases/2.1.246/linux-x64/claude.zst

Anything sustaining under ~450 KB/s cannot finish 78,105,392 bytes inside the 180s window.

To confirm nothing is wrong with the environment itself (all verified here):

  • Server binary is -rwxr-xr-x and runs: ~/.claude/remote/srv/*/server --version responds correctly
  • Root filesystem is ext4 rw, no noexec
  • 951GB free disk
  • env -i /bin/sh -c 'command -v claude' resolves the locally installed CLI

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

N/A

Claude Code Version

2.1.250 (Claude Code) — locally installed in WSL. Desktop app pins CLI 2.1.246; remote server claude-ssh 7d193f89fc02cf1035a391245312e34ad419f63e (built 2026-08-25).

Platform

Other

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

Note on repo: this affects Claude Desktop's WSL remote deployment rather than the Claude Code CLI itself. Filing here since this is the closest public tracker — happy to move it if there's a better place.

Workaround that works

Pre-seed the exact pinned version so --install reports cliWasPresent:true and the download is skipped entirely:

mkdir -p ~/.claude/remote/ccd-cli
curl -L -o /tmp/claude.zst https://downloads.claude.ai/claude-code-releases/2.1.246/linux-x64/claude.zst
unzstd -o ~/.claude/remote/ccd-cli/2.1.246 /tmp/claude.zst
chmod +x ~/.claude/remote/ccd-cli/2.1.246

Verified: server --install --cli-dir ~/.claude/remote/ccd-cli --cli-version 2.1.246 then returns
{"cliPath":"/home/ayman/.claude/remote/ccd-cli/2.1.246","cliWasPresent":true}, and the session opens immediately.

This has to be repeated every time the desktop app pins a new CLI version.

Suggested fixes

  1. Stream download progress from the remote fetch (bytes received / total) the way SFTP uploads already do, and make the idle timeout measure stalled progress rather than wall-clock since the last message.
  2. Serialize setups per target (don't start start_session while warm_up is still deploying), and have an aborted install terminate its remote fetch.
  3. Make pruneCliDir / sweepOrphans ignore .fetch-* files younger than a few minutes, or lock the directory, so concurrent runs can't delete an in-flight download.
  4. Prefer the SFTP fallback (which has progress reporting) up front when a previous remote fetch timed out, instead of retrying the same remote fetch.
  5. Optionally, reuse a compatible CLI already installed on the machine (e.g. ~/.local/share/claude/versions/<version>) when the version matches.

View original on GitHub ↗