Skip to content

Windows: EPERM when renaming temporary SSH config file #840

@blinkagent

Description

@blinkagent

Description

On Windows, the extension fails with EPERM when attempting to atomically rename the temporary SSH config file to the final destination:

Failed to rename temporary SSH config file at C:\Users\<user>\.ssh/.config.vscode-coder-tmp.<random>
to C:\Users\<user>\.ssh\config: EPERM: operation not permitted, rename
'C:\Users\<user>\.ssh\.config.vscode-coder-tmp.<random>' -> 'C:\Users\<user>\.ssh\config'.
Please check your disk space, permissions, and that the directory exists.

This was observed after a Coder server upgrade (2.27 → 2.29). The user was running Coder CLI v2.23.1 on their Windows desktop. Deleting the SSH config file and letting the extension regenerate it resolved the issue.

Root Cause

The save() method in src/remote/sshConfig.ts writes to a temp file and then calls fs.rename() to atomically replace the real config. On Windows, fs.rename() fails with EPERM if another process holds a lock on the target file. Common causes:

  1. Antivirus / EDR software (Windows Defender, CrowdStrike, etc.) performing real-time scanning on the SSH config file
  2. Windows Search Indexer briefly locking files in user profile directories
  3. Concurrent access from another SSH-aware tool, Git client, or another VS Code window reading/writing the same config
  4. The Coder CLI (coder config-ssh) also manages blocks in the same ~/.ssh/config — if both write simultaneously (e.g., during a server upgrade reconnection), they can race

Suggested Fix

Add retry logic to the rename() call in save() with a short delay between attempts. This is a well-known pattern for handling transient EPERM/EACCES errors on Windows caused by antivirus or indexer file locks. For example:

async function renameWithRetry(
  src: string,
  dest: string,
  retries = 5,
  delayMs = 200,
): Promise<void> {
  for (let i = 0; i < retries; i++) {
    try {
      await rename(src, dest);
      return;
    } catch (err: unknown) {
      const code = (err as NodeJS.ErrnoException).code;
      if ((code === "EPERM" || code === "EACCES") && i < retries - 1) {
        await new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1)));
        continue;
      }
      throw err;
    }
  }
}

This is the same approach used by VS Code's own test framework and npm/node-graceful-fs to handle this class of Windows file system issue.

Environment

  • OS: Windows
  • Coder CLI: v2.23.1
  • Coder Server: upgraded from v2.27 to v2.29
  • Trigger: Server upgrade caused extension to update SSH config; rename failed on locked file

Created on behalf of @matifali

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions