Skip to content

Commit b1f4eb0

Browse files
committed
Add support to coder.sshFlags array for user configurable SSH options
1 parent 748b130 commit b1f4eb0

File tree

4 files changed

+131
-32
lines changed

4 files changed

+131
-32
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,18 @@
130130
],
131131
"markdownEnumDescriptions": [
132132
"Disables autostart on macOS only (recommended to avoid sleep/wake issues)",
133-
"Disables on all platforms",
133+
"Disables autostart on all platforms",
134134
"Keeps autostart enabled on all platforms"
135135
],
136136
"default": "auto"
137137
},
138+
"coder.sshFlags": {
139+
"markdownDescription": "Additional flags to pass to the `coder ssh` command when establishing SSH connections. Enter each flag as a separate array item; values are passed verbatim and in order. See the [CLI ssh reference](https://coder.com/docs/reference/cli/ssh) for available flags.\n\nNote that the following flags are ignored because they are managed internally by the extension: `--network-info-dir`, `--log-dir`, `--ssh-host-prefix`. To manage the `--disable-autostart` flag, use the `coder.disableAutostart` setting.",
140+
"type": "array",
141+
"items": {
142+
"type": "string"
143+
}
144+
},
138145
"coder.globalFlags": {
139146
"markdownDescription": "Global flags to pass to every Coder CLI invocation. Enter each flag as a separate array item; values are passed verbatim and in order. Do **not** include the `coder` command itself. See the [CLI reference](https://coder.com/docs/reference/cli) for available global flags.\n\nNote that for `--header-command`, precedence is: `#coder.headerCommand#` setting, then `CODER_HEADER_COMMAND` environment variable, then the value specified here. The `--global-config` flag is explicitly ignored.",
140147
"type": "array",

src/cliConfig.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,10 @@ export function shouldDisableAutostart(
3838
);
3939
return setting === "always" || (setting === "auto" && platform === "darwin");
4040
}
41+
42+
/**
43+
* Returns SSH flags for the `coder ssh` command from user configuration.
44+
*/
45+
export function getSshFlags(configs: WorkspaceConfiguration): string[] {
46+
return configs.get<string[]>("coder.sshFlags") || [];
47+
}

src/remote/remote.ts

Lines changed: 85 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ import {
2020
import { extractAgents } from "../api/api-helper";
2121
import { CoderApi } from "../api/coderApi";
2222
import { needToken } from "../api/utils";
23-
import { getGlobalFlags, shouldDisableAutostart } from "../cliConfig";
23+
import {
24+
getGlobalFlags,
25+
getSshFlags,
26+
shouldDisableAutostart,
27+
} from "../cliConfig";
2428
import { type Commands } from "../commands";
2529
import { type CliManager } from "../core/cliManager";
2630
import * as cliUtils from "../core/cliUtils";
@@ -571,16 +575,85 @@ export class Remote {
571575
}
572576

573577
/**
574-
* Formats the --log-dir argument for the ProxyCommand after making sure it
578+
* Builds the ProxyCommand for SSH connections to Coder workspaces.
579+
* Uses `coder ssh` for modern deployments with wildcard support,
580+
* or falls back to `coder vscodessh` for older deployments.
581+
*/
582+
private async buildProxyCommand(
583+
binaryPath: string,
584+
label: string,
585+
hostPrefix: string,
586+
logDir: string,
587+
useWildcardSSH: boolean,
588+
): Promise<string> {
589+
const vscodeConfig = vscode.workspace.getConfiguration();
590+
591+
const escapedBinaryPath = escapeCommandArg(binaryPath);
592+
const globalConfig = getGlobalFlags(
593+
vscodeConfig,
594+
this.pathResolver.getGlobalConfigDir(label),
595+
);
596+
const logArgs = await this.getLogArgs(logDir);
597+
598+
if (useWildcardSSH) {
599+
// User SSH flags are included first; internally-managed flags
600+
// are appended last so they take precedence.
601+
const userSSHFlags = getSshFlags(vscodeConfig);
602+
const disableAutostart = shouldDisableAutostart(
603+
vscodeConfig,
604+
process.platform,
605+
)
606+
? ["--disable-autostart"]
607+
: [];
608+
const internalFlags = [
609+
"--stdio",
610+
"--usage-app=vscode",
611+
"--network-info-dir",
612+
escapeCommandArg(this.pathResolver.getNetworkInfoPath()),
613+
...logArgs,
614+
...disableAutostart,
615+
"--ssh-host-prefix",
616+
hostPrefix,
617+
"%h",
618+
];
619+
620+
const allFlags = [...userSSHFlags, ...internalFlags];
621+
return `${escapedBinaryPath} ${globalConfig.join(" ")} ssh ${allFlags.join(" ")}`;
622+
} else {
623+
const networkInfoDir = escapeCommandArg(
624+
this.pathResolver.getNetworkInfoPath(),
625+
);
626+
const sessionTokenFile = escapeCommandArg(
627+
this.pathResolver.getSessionTokenPath(label),
628+
);
629+
const urlFile = escapeCommandArg(this.pathResolver.getUrlPath(label));
630+
631+
const sshFlags = [
632+
"--network-info-dir",
633+
networkInfoDir,
634+
...logArgs,
635+
"--session-token-file",
636+
sessionTokenFile,
637+
"--url-file",
638+
urlFile,
639+
"%h",
640+
];
641+
642+
return `${escapedBinaryPath} ${globalConfig.join(" ")} vscodessh ${sshFlags.join(" ")}`;
643+
}
644+
}
645+
646+
/**
647+
* Returns the --log-dir argument for the ProxyCommand after making sure it
575648
* has been created.
576649
*/
577-
private async formatLogArg(logDir: string): Promise<string> {
650+
private async getLogArgs(logDir: string): Promise<string[]> {
578651
if (!logDir) {
579-
return "";
652+
return [];
580653
}
581654
await fs.mkdir(logDir, { recursive: true });
582655
this.logger.info("SSH proxy diagnostics are being written to", logDir);
583-
return ` --log-dir ${escapeCommandArg(logDir)} -v`;
656+
return ["--log-dir", escapeCommandArg(logDir), "-v"];
584657
}
585658

586659
// updateSSHConfig updates the SSH configuration with a wildcard that handles
@@ -666,15 +739,13 @@ export class Remote {
666739
? `${AuthorityPrefix}.${label}--`
667740
: `${AuthorityPrefix}--`;
668741

669-
const globalConfigs = this.globalConfigs(label);
670-
671-
const proxyCommand = featureSet.wildcardSSH
672-
? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode${this.disableAutostartConfig()} --network-info-dir ${escapeCommandArg(this.pathResolver.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
673-
: `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg(
674-
this.pathResolver.getNetworkInfoPath(),
675-
)}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.pathResolver.getSessionTokenPath(label))} --url-file ${escapeCommandArg(
676-
this.pathResolver.getUrlPath(label),
677-
)} %h`;
742+
const proxyCommand = await this.buildProxyCommand(
743+
binaryPath,
744+
label,
745+
hostPrefix,
746+
logDir,
747+
featureSet.wildcardSSH,
748+
);
678749

679750
const sshValues: SSHValues = {
680751
Host: hostPrefix + `*`,
@@ -727,15 +798,6 @@ export class Remote {
727798
return sshConfig.getRaw();
728799
}
729800

730-
private globalConfigs(label: string): string {
731-
const vscodeConfig = vscode.workspace.getConfiguration();
732-
const args = getGlobalFlags(
733-
vscodeConfig,
734-
this.pathResolver.getGlobalConfigDir(label),
735-
);
736-
return ` ${args.join(" ")}`;
737-
}
738-
739801
private watchLogDirSetting(
740802
currentLogDir: string,
741803
featureSet: FeatureSet,
@@ -812,13 +874,6 @@ export class Remote {
812874
return [statusBarItem, agentWatcher, onChangeDisposable];
813875
}
814876

815-
private disableAutostartConfig(): string {
816-
const configs = vscode.workspace.getConfiguration();
817-
return shouldDisableAutostart(configs, process.platform)
818-
? " --disable-autostart"
819-
: "";
820-
}
821-
822877
// closeRemote ends the current remote session.
823878
public async closeRemote() {
824879
await vscode.commands.executeCommand("workbench.action.remote.close");

test/unit/cliConfig.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { it, expect, describe } from "vitest";
22
import { type WorkspaceConfiguration } from "vscode";
33

4-
import { getGlobalFlags, shouldDisableAutostart } from "@/cliConfig";
4+
import {
5+
getGlobalFlags,
6+
getSshFlags,
7+
shouldDisableAutostart,
8+
} from "@/cliConfig";
59

610
import { isWindows } from "../utils/platform";
711

@@ -123,6 +127,32 @@ describe("cliConfig", () => {
123127
expect(shouldDisableAutostart(config, "linux")).toBe(false);
124128
});
125129
});
130+
131+
describe("getSshFlags", () => {
132+
it("returns empty array when no SSH flags configured", () => {
133+
const config = {
134+
get: () => undefined,
135+
} as unknown as WorkspaceConfiguration;
136+
137+
expect(getSshFlags(config)).toStrictEqual([]);
138+
});
139+
140+
it("returns SSH flags from config", () => {
141+
const config = {
142+
get: (key: string) =>
143+
key === "coder.sshFlags"
144+
? ["--disable-autostart", "--wait=yes", "--ssh-host-prefix=custom"]
145+
: undefined,
146+
} as unknown as WorkspaceConfiguration;
147+
148+
expect(getSshFlags(config)).toStrictEqual([
149+
"--disable-autostart",
150+
"--wait=yes",
151+
// No filtering and returned as-is (even though it'll be overridden later)
152+
"--ssh-host-prefix=custom",
153+
]);
154+
});
155+
});
126156
});
127157

128158
function quoteCommand(value: string): string {

0 commit comments

Comments
 (0)