Skip to content

Commit 53ae245

Browse files
committed
fix: ignore the SSH config file setting where ssh never reads it
Antigravity and Windsurf/Devin renamed the setting to remote.antigravitySSH.configFile and remote.devinSSH.configFile, then spawn ssh without -F, so ssh reads ~/.ssh/config no matter what any of them say. The setting only feeds their own host tree. Honoring it, or a stale remote.SSH.configFile synced in from another editor, writes the workspace host to a file the connection never reads. Ignore it on those two and keep reading remote.SSH.configFile elsewhere: Microsoft's extension and Cursor's fork pass it to ssh with -F, and VSCodium's fork parses the file itself instead of running ssh. This drops the per-extension section map from #1060: the three extensions that do connect through the setting all read remote.SSH.
1 parent 7cd50af commit 53ae245

5 files changed

Lines changed: 56 additions & 63 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,10 @@
3232

3333
### Fixed
3434

35-
- Write the SSH config to the file the active Remote-SSH extension actually
36-
reads. Windsurf/Devin and Antigravity renamed the whole `remote.SSH` settings
37-
section, so a custom config file set as `remote.devinSSH.configFile`,
38-
`remote.windsurfSSH.configFile`, or `remote.antigravitySSH.configFile` was
39-
ignored and the workspace host was written to `~/.ssh/config` instead, where
40-
those editors never looked for it.
35+
- Ignore the SSH config file setting on Antigravity and Windsurf/Devin. They
36+
launch ssh without pointing it at a config file, so it always reads
37+
`~/.ssh/config`, and honoring the setting wrote the workspace host where the
38+
connection never looked.
4139
- Apply a 60-second default timeout to REST requests, so requests hung on a
4240
half-open TCP connection don't stall pollers forever.
4341
- Change `coder.binarySource`, `coder.binaryDestination`, `coder.headerCommand`,

CONTRIBUTING.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ Host coder-vscode.dev.coder.com--*
4141
LogLevel ERROR
4242
```
4343

44+
Which file that entry goes in depends on the Remote - SSH extension. Microsoft's
45+
and Cursor's pass `remote.SSH.configFile` to ssh with `-F`, and VSCodium's parses
46+
the file itself instead of running ssh, so all three connect through it.
47+
Antigravity and Windsurf/Devin renamed the setting but spawn ssh without `-F`, so
48+
ssh reads `~/.ssh/config` regardless; we ignore it there rather than write the
49+
host where the connection never looks.
50+
4451
If any step fails, we show an error message. Once the error message is closed
4552
we close the remote so the Remote - SSH connection does not continue to
4653
connection. Otherwise, we yield, which lets the Remote - SSH continue.

src/remote/remote.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import {
5555
parseCoderSshOptions,
5656
parseSshConfig,
5757
} from "./sshConfig";
58-
import { getRemoteSshSetting } from "./sshExtension";
58+
import { getRemoteSshConfigFile } from "./sshExtension";
5959
import { applySettingOverrides, buildSshOverrides } from "./sshOverrides";
6060
import { SshProcessMonitor } from "./sshProcess";
6161
import { computeSshProperties, sshSupportsSetEnv } from "./sshSupport";
@@ -930,7 +930,7 @@ export class Remote {
930930
}
931931

932932
private getSshConfigPath(): string {
933-
const configured = getRemoteSshSetting("configFile");
933+
const configured = getRemoteSshConfigFile();
934934
return expandPath(configured || path.join("~", ".ssh", "config"));
935935
}
936936

src/remote/sshExtension.ts

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,25 @@ export const REMOTE_SSH_EXTENSION_IDS = [
1111
export type RemoteSshExtensionId = (typeof REMOTE_SSH_EXTENSION_IDS)[number];
1212

1313
/**
14-
* Sections each extension reads, in order. The rebranded forks renamed the
15-
* whole `remote.SSH` section, so reading it directly misses them.
14+
* Extensions that spawn ssh without `-F`, so it reads ~/.ssh/config whatever
15+
* their renamed setting says. Honoring one would write the workspace host
16+
* where the connection never looks.
1617
*/
17-
const SETTING_SECTIONS: Readonly<
18-
Record<RemoteSshExtensionId, readonly string[]>
19-
> = {
20-
"jeanp413.open-remote-ssh": ["remote.SSH"],
21-
// Windsurf became Devin and reads both, preferring the new name.
22-
"codeium.windsurf-remote-openssh": ["remote.devinSSH", "remote.windsurfSSH"],
23-
"anysphere.remote-ssh": ["remote.SSH"],
24-
"ms-vscode-remote.remote-ssh": ["remote.SSH"],
25-
"google.antigravity-remote-openssh": ["remote.antigravitySSH"],
26-
};
18+
const IGNORED_CONFIG_FILE: readonly RemoteSshExtensionId[] = [
19+
"google.antigravity-remote-openssh",
20+
"codeium.windsurf-remote-openssh",
21+
];
2722

28-
/** First non-empty value for a string setting, e.g. `configFile`. */
29-
export function getRemoteSshSetting(key: string): string | undefined {
23+
/** The SSH config file the active extension connects through, if configured. */
24+
export function getRemoteSshConfigFile(): string | undefined {
3025
const id = getRemoteSshExtension()?.id;
31-
const sections = id ? SETTING_SECTIONS[id] : ["remote.SSH"];
32-
for (const section of sections) {
33-
const value = vscode.workspace.getConfiguration(section).get<string>(key);
34-
if (value) {
35-
return value;
36-
}
26+
if (id && IGNORED_CONFIG_FILE.includes(id)) {
27+
return undefined;
3728
}
38-
return undefined;
29+
return (
30+
vscode.workspace.getConfiguration("remote.SSH").get<string>("configFile") ||
31+
undefined
32+
);
3933
}
4034

4135
/**
Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, vi } from "vitest";
22
import * as vscode from "vscode";
33

4-
import { getRemoteSshSetting } from "@/remote/sshExtension";
4+
import { getRemoteSshConfigFile } from "@/remote/sshExtension";
55

66
import { config, type Settings } from "../../mocks/testHelpers";
77

@@ -13,47 +13,41 @@ function setup(extensionId: string, settings: Settings = {}): void {
1313
);
1414
}
1515

16-
describe("getRemoteSshSetting", () => {
16+
describe("getRemoteSshConfigFile", () => {
1717
it.each([
18-
["ms-vscode-remote.remote-ssh", "remote.SSH.configFile"],
19-
["anysphere.remote-ssh", "remote.SSH.configFile"],
20-
["jeanp413.open-remote-ssh", "remote.SSH.configFile"],
21-
["google.antigravity-remote-openssh", "remote.antigravitySSH.configFile"],
22-
["codeium.windsurf-remote-openssh", "remote.devinSSH.configFile"],
23-
])("reads the section %s uses", (extensionId, settingKey) => {
24-
setup(extensionId, { [settingKey]: "/custom/config" });
18+
"ms-vscode-remote.remote-ssh",
19+
"anysphere.remote-ssh",
20+
"jeanp413.open-remote-ssh",
21+
])("reads the configured file for %s", (extensionId) => {
22+
setup(extensionId, { "remote.SSH.configFile": "/custom/config" });
2523

26-
expect(getRemoteSshSetting("configFile")).toBe("/custom/config");
24+
expect(getRemoteSshConfigFile()).toBe("/custom/config");
2725
});
2826

29-
it("falls back to the legacy Windsurf section", () => {
30-
setup("codeium.windsurf-remote-openssh", {
31-
"remote.windsurfSSH.configFile": "/legacy/config",
32-
});
33-
34-
expect(getRemoteSshSetting("configFile")).toBe("/legacy/config");
35-
});
36-
37-
it("prefers the Devin section over the legacy Windsurf one", () => {
38-
setup("codeium.windsurf-remote-openssh", {
39-
"remote.devinSSH.configFile": "/devin/config",
40-
"remote.windsurfSSH.configFile": "/legacy/config",
41-
});
42-
43-
expect(getRemoteSshSetting("configFile")).toBe("/devin/config");
44-
});
27+
it.each([
28+
["google.antigravity-remote-openssh", "remote.antigravitySSH.configFile"],
29+
["codeium.windsurf-remote-openssh", "remote.devinSSH.configFile"],
30+
])(
31+
"ignores the file %s never connects through",
32+
(extensionId, settingKey) => {
33+
setup(extensionId, {
34+
[settingKey]: "/custom/config",
35+
"remote.SSH.configFile": "/stale/config",
36+
});
37+
38+
expect(getRemoteSshConfigFile()).toBeUndefined();
39+
},
40+
);
4541

46-
it("ignores another extension's section", () => {
47-
setup("google.antigravity-remote-openssh", {
48-
"remote.SSH.configFile": "/custom/config",
49-
});
42+
it("reads remote.SSH when no extension is installed", () => {
43+
setup("", { "remote.SSH.configFile": "/custom/config" });
5044

51-
expect(getRemoteSshSetting("configFile")).toBeUndefined();
45+
expect(getRemoteSshConfigFile()).toBe("/custom/config");
5246
});
5347

54-
it("defaults to remote.SSH when no extension is installed", () => {
55-
setup("", { "remote.SSH.configFile": "/custom/config" });
48+
it("returns undefined when nothing is configured", () => {
49+
setup("ms-vscode-remote.remote-ssh");
5650

57-
expect(getRemoteSshSetting("configFile")).toBe("/custom/config");
51+
expect(getRemoteSshConfigFile()).toBeUndefined();
5852
});
5953
});

0 commit comments

Comments
 (0)