Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- Always enable verbose (`-v`) flag when a log directory is configured (`coder.proxyLogDir`).

### Added

- Add support for CLI global flag configurations through the `coder.globalFlags` setting.

## [1.10.1](https://github.com/coder/vscode-coder/releases/tag/v1.10.1) 2025-08-13

### Fixed
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@
"markdownDescription": "Disable Coder CLI signature verification, which can be useful if you run an unsigned fork of the binary.",
"type": "boolean",
"default": false
},
"coder.globalFlags": {
"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.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
6 changes: 2 additions & 4 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as ws from "ws";
import { errToStr } from "./api-helper";
import { CertificateError } from "./error";
import { FeatureSet } from "./featureSet";
import { getHeaderArgs } from "./headers";
import { getGlobalFlags } from "./globalFlags";
import { getProxyForUrl } from "./proxy";
import { Storage } from "./storage";
import { expandPath } from "./util";
Expand Down Expand Up @@ -186,9 +186,7 @@ export async function startWorkspaceIfStoppedOrFailed(

return new Promise((resolve, reject) => {
const startArgs = [
"--global-config",
globalConfigDir,
...getHeaderArgs(vscode.workspace.getConfiguration()),
...getGlobalFlags(vscode.workspace.getConfiguration(), globalConfigDir),
"start",
"--yes",
workspace.owner_name + "/" + workspace.name,
Expand Down
17 changes: 11 additions & 6 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import * as vscode from "vscode";
import { makeCoderSdk, needToken } from "./api";
import { extractAgents } from "./api-helper";
import { CertificateError } from "./error";
import { getGlobalFlags } from "./globalFlags";
import { Storage } from "./storage";
import { toRemoteAuthority, toSafeHost } from "./util";
import { escapeCommandArg, toRemoteAuthority, toSafeHost } from "./util";
import {
AgentTreeItem,
WorkspaceTreeItem,
Expand Down Expand Up @@ -503,12 +504,16 @@ export class Commands {
this.restClient,
toSafeHost(url),
);
const escape = (str: string): string =>
`"${str.replace(/"/g, '\\"')}"`;

const configDir = path.dirname(
this.storage.getSessionTokenPath(toSafeHost(url)),
);
const globalFlags = getGlobalFlags(
vscode.workspace.getConfiguration(),
configDir,
);
terminal.sendText(
`${escape(binary)} ssh --global-config ${escape(
path.dirname(this.storage.getSessionTokenPath(toSafeHost(url))),
)} ${app.workspace_name}`,
`${escapeCommandArg(binary)}${globalFlags.length === 0 ? "" : ` ${globalFlags.join(" ")}`} ssh ${app.workspace_name}`,
);
await new Promise((resolve) => setTimeout(resolve, 5000));
terminal.sendText(app.command ?? "");
Expand Down
78 changes: 78 additions & 0 deletions src/globalFlags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { it, expect, describe } from "vitest";
import { WorkspaceConfiguration } from "vscode";
import { getGlobalFlags } from "./globalFlags";

describe("Global flags suite", () => {
it("should return global-config and header args when no global flags configured", () => {
const config = {
get: () => undefined,
} as unknown as WorkspaceConfiguration;

expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
"--global-config",
'"/config/dir"',
]);
});

it("should return global flags from config with global-config appended", () => {
const config = {
get: (key: string) =>
key === "coder.globalFlags"
? ["--verbose", "--disable-direct-connections"]
: undefined,
} as unknown as WorkspaceConfiguration;

expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
"--verbose",
"--disable-direct-connections",
"--global-config",
'"/config/dir"',
]);
});

it("should not filter duplicate global-config flags, last takes precedence", () => {
const config = {
get: (key: string) =>
key === "coder.globalFlags"
? [
"-v",
"--global-config /path/to/ignored",
"--disable-direct-connections",
]
: undefined,
} as unknown as WorkspaceConfiguration;

expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([
"-v",
"--global-config /path/to/ignored",
"--disable-direct-connections",
"--global-config",
'"/config/dir"',
]);
});

it("should not filter header-command flags, header args appended at end", () => {
const config = {
get: (key: string) => {
if (key === "coder.headerCommand") {
return "echo test";
}
if (key === "coder.globalFlags") {
return ["-v", "--header-command custom", "--no-feature-warning"];
}
return undefined;
},
} as unknown as WorkspaceConfiguration;

const result = getGlobalFlags(config, "/config/dir");
expect(result).toStrictEqual([
"-v",
"--header-command custom", // ignored by CLI
"--no-feature-warning",
"--global-config",
'"/config/dir"',
"--header-command",
"'echo test'",
]);
});
});
15 changes: 15 additions & 0 deletions src/globalFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { WorkspaceConfiguration } from "vscode";
import { getHeaderArgs } from "./headers";
import { escapeCommandArg } from "./util";

export function getGlobalFlags(
configs: WorkspaceConfiguration,
configDir: string,
): string[] {
// Last takes precedence/overrides previous ones
return [
...(configs.get<string[]>("coder.globalFlags") || []),
...["--global-config", escapeCommandArg(configDir)],
...getHeaderArgs(configs),
];
}
23 changes: 14 additions & 9 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { extractAgents } from "./api-helper";
import * as cli from "./cliManager";
import { Commands } from "./commands";
import { featureSetForVersion, FeatureSet } from "./featureSet";
import { getHeaderArgs } from "./headers";
import { getGlobalFlags } from "./globalFlags";
import { Inbox } from "./inbox";
import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig";
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport";
Expand Down Expand Up @@ -758,19 +758,15 @@ export class Remote {
const sshConfig = new SSHConfig(sshConfigFile);
await sshConfig.load();

const headerArgs = getHeaderArgs(vscode.workspace.getConfiguration());
const headerArgList =
headerArgs.length > 0 ? ` ${headerArgs.join(" ")}` : "";

const hostPrefix = label
? `${AuthorityPrefix}.${label}--`
: `${AuthorityPrefix}--`;

const globalConfigs = this.globalConfigs(label);

const proxyCommand = featureSet.wildcardSSH
? `${escapeCommandArg(binaryPath)}${headerArgList} --global-config ${escapeCommandArg(
path.dirname(this.storage.getSessionTokenPath(label)),
)} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
: `${escapeCommandArg(binaryPath)}${headerArgList} vscodessh --network-info-dir ${escapeCommandArg(
? `${escapeCommandArg(binaryPath)}${globalConfigs} ssh --stdio --usage-app=vscode --disable-autostart --network-info-dir ${escapeCommandArg(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
: `${escapeCommandArg(binaryPath)}${globalConfigs} vscodessh --network-info-dir ${escapeCommandArg(
this.storage.getNetworkInfoPath(),
)}${await this.formatLogArg(logDir)} --session-token-file ${escapeCommandArg(this.storage.getSessionTokenPath(label))} --url-file ${escapeCommandArg(
this.storage.getUrlPath(label),
Expand Down Expand Up @@ -828,6 +824,15 @@ export class Remote {
return sshConfig.getRaw();
}

private globalConfigs(label: string): string {
const vscodeConfig = vscode.workspace.getConfiguration();
const args: string[] = getGlobalFlags(
vscodeConfig,
path.dirname(this.storage.getSessionTokenPath(label)),
);
return args.length === 0 ? "" : ` ${args.join(" ")}`;
}

// showNetworkUpdates finds the SSH process ID that is being used by this
// workspace and reads the file being created by the Coder CLI.
private showNetworkUpdates(sshPid: number): vscode.Disposable {
Expand Down