From 83935e1234233738fe410c01cdfbfc169803ca69 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Sun, 2 Aug 2026 20:44:02 +0000 Subject: [PATCH 1/9] Add --trust-folder CLI argument to preload Workspace Trust Adds a repeatable --trust-folder argument that marks the given folder(s) as trusted before the window opens and persists them, so a workspace can be opened trusted without going through the trust dialog. Implements #126535. --- src/vs/platform/environment/common/argv.ts | 1 + .../environment/common/environmentService.ts | 3 + src/vs/platform/environment/node/argv.ts | 1 + .../environment/browser/environmentService.ts | 3 + .../environment/common/environmentService.ts | 1 + .../workspaces/common/workspaceTrust.ts | 35 +++- .../test/common/workspaceTrust.test.ts | 154 +++++++++++++++++- 7 files changed, 194 insertions(+), 4 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 18d653bac95609..801537b24bb03b 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -121,6 +121,7 @@ export interface NativeParsedArgs { 'use-inmemory-secretstorage'?: boolean; 'password-store'?: string; 'disable-workspace-trust'?: boolean; + 'trust-folder'?: string[]; 'disable-crash-reporter'?: boolean; 'crash-reporter-directory'?: string; 'crash-reporter-id'?: string; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 004d0614c938a3..d309e5c1e30bab 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -260,6 +260,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } + @memoize + get trustedFolders(): string[] { return this.args['trust-folder'] || []; } + @memoize get useInMemorySecretStorage(): boolean { return !!this.args['use-inmemory-secretstorage']; } diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 5933c9c71d9473..3c3e5942ea9ae5 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -191,6 +191,7 @@ export const OPTIONS: OptionDescriptions> = { 'use-inmemory-secretstorage': { type: 'boolean', deprecates: ['disable-keytar'] }, 'password-store': { type: 'string' }, 'disable-workspace-trust': { type: 'boolean' }, + 'trust-folder': { type: 'string[]', cat: 'o', args: 'folder', description: localize('trustFolder', "Trust the given folder and its subfolders for Workspace Trust. Accepts a folder path or URI, can be repeated, and is persisted so the folder stays trusted when reopened.") }, 'disable-crash-reporter': { type: 'boolean' }, 'crash-reporter-directory': { type: 'string' }, 'crash-reporter-id': { type: 'string' }, diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 98215099c768f2..31f87f0d8b0463 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -268,6 +268,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get disableWorkspaceTrust(): boolean { return !this.options.enableWorkspaceTrust; } + @memoize + get trustedFolders(): string[] { return []; } + @memoize get isSessionsWindow(): boolean { return this.payload?.get('isSessionsWindow') === 'true'; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 3ad7edaa434056..90fa7e99403ab1 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -35,6 +35,7 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly skipReleaseNotes: boolean; readonly skipWelcome: boolean; readonly disableWorkspaceTrust: boolean; + readonly trustedFolders: string[]; readonly isSessionsWindow: boolean; readonly webviewExternalEndpoint: string; diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index cc10c260ddf150..28fff4c9cc0216 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -156,7 +156,9 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._workspaceResolvedPromiseResolve(); if (!this.environmentService.remoteAuthority) { - this._workspaceTrustInitializedPromiseResolve(); + // Persist folders passed via `--trust-folder` before signalling that + // trust is initialized, so they are already trusted at startup. + this.addTrustedFoldersFromCli().finally(() => this._workspaceTrustInitializedPromiseResolve()); } }); @@ -169,7 +171,10 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); }) .finally(() => { - this._workspaceTrustInitializedPromiseResolve(); + // The remote authority is now resolved, so `--trust-folder` values + // for remote folders can be canonicalized before signalling that + // trust is initialized. + this.addTrustedFoldersFromCli().finally(() => this._workspaceTrustInitializedPromiseResolve()); }); } @@ -282,6 +287,32 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); } + private async addTrustedFoldersFromCli(): Promise { + const folders = this.environmentService.trustedFolders; + if (!folders?.length) { + return; + } + + const uris: URI[] = []; + for (const folder of folders) { + try { + // A value with a scheme (e.g. a remote `vscode-remote://` folder) is + // parsed as a Uri; otherwise it is treated as a local file path. + uris.push(folder.includes('://') ? URI.parse(folder) : URI.file(folder)); + } catch { + // Ignore malformed --trust-folder arguments + } + } + + if (uris.length) { + try { + await this.setUrisTrust(uris, true); + } catch { + // Never block workspace trust initialization on a bad --trust-folder value + } + } + } + private getWorkspaceUris(): URI[] { const workspaceUris = this._canonicalWorkspace.folders.map(f => f.uri); const workspaceConfiguration = this._canonicalWorkspace.configuration; diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 8921cb040808a9..c3ea4bf65775a3 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -9,13 +9,14 @@ import { mock } from '../../../../../base/test/common/mock.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { IRemoteAuthorityResolverService } from '../../../../../platform/remote/common/remoteAuthorityResolver.js'; +import { IRemoteAuthorityResolverService, ResolverResult } from '../../../../../platform/remote/common/remoteAuthorityResolver.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkspaceTrustEnablementService, IWorkspaceTrustInfo } from '../../../../../platform/workspace/common/workspaceTrust.js'; -import { Workspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; +import { Workspace, testWorkspace } from '../../../../../platform/workspace/test/common/testWorkspace.js'; import { Memento } from '../../../../common/memento.js'; import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js'; import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -45,6 +46,7 @@ suite('Workspace Trust', () => { const fileService = store.add(new FileService(new NullLogService())); const uriIdentityService = store.add(new UriIdentityService(fileService)); + instantiationService.stub(IFileService, fileService); instantiationService.stub(IUriIdentityService, uriIdentityService); instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { }); }); @@ -159,6 +161,154 @@ suite('Workspace Trust', () => { assert.strictEqual(true, (await testObject.getUriTrustInfo(sameFolderDifferentMeta)).trusted); }); + test('trust folders passed via --trust-folder', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const folder = URI.file('/trusted-from-cli'); + environmentService.trustedFolders = [folder.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(folder)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, testObject.isWorkspaceTrusted()); + assert.strictEqual(true, (await testObject.getUriTrustInfo(folder)).trusted); + }); + + test('trust folders passed via --trust-folder (subfolder is trusted)', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const parent = URI.file('/trusted-parent'); + environmentService.trustedFolders = [parent.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(URI.file('/trusted-parent/child'))); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, testObject.isWorkspaceTrusted()); + }); + + test('trust multiple folders passed via --trust-folder', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const a = URI.file('/cli-a'); + const b = URI.file('/cli-b'); + environmentService.trustedFolders = [a.fsPath, b.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(a)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, (await testObject.getUriTrustInfo(a)).trusted); + assert.strictEqual(true, (await testObject.getUriTrustInfo(b)).trusted); + }); + + test('trusts a multi-root workspace when --trust-folder covers all roots', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const rootA = URI.file('/multi-a'); + const rootB = URI.file('/multi-b'); + environmentService.trustedFolders = [rootA.fsPath, rootB.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(rootA, rootB)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, testObject.isWorkspaceTrusted()); + }); + + test('does not trust a multi-root workspace when --trust-folder covers only some roots', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const rootA = URI.file('/multi-a'); + const rootB = URI.file('/multi-b'); + environmentService.trustedFolders = [rootA.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(rootA, rootB)); + const testObject = await initializeTestObject(); + + assert.strictEqual(false, testObject.isWorkspaceTrusted()); + assert.strictEqual(true, (await testObject.getUriTrustInfo(rootA)).trusted); + assert.strictEqual(false, (await testObject.getUriTrustInfo(rootB)).trusted); + }); + + test('trusts a remote (vscode-remote://) folder passed via --trust-folder', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const remoteAuthority = 'test+auth'; + const remoteFolder = URI.parse(`vscode-remote://${remoteAuthority}/home/me/proj`); + + environmentService.remoteAuthority = remoteAuthority; + environmentService.trustedFolders = [`vscode-remote://${remoteAuthority}/home/me/proj`]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + // A resolver that is reachable but does not mark the remote as trusted, so + // trust must come from the --trust-folder entry rather than the remote itself. + instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { + override async resolveAuthority(authority: string): Promise { + return { authority: { authority } } as unknown as ResolverResult; + } + override async getCanonicalURI(uri: URI): Promise { + return uri; + } + }); + + workspaceService.setWorkspace(testWorkspace(remoteFolder)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, testObject.isWorkspaceTrusted()); + assert.strictEqual(true, (await testObject.getUriTrustInfo(remoteFolder)).trusted); + }); + + test('folders passed via --trust-folder persist across reloads', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const folder = URI.file('/trusted-persist'); + environmentService.trustedFolders = [folder.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(folder)); + await initializeTestObject(); + + // A subsequent window without the flag still trusts the folder because + // it was persisted to the trusted folder list. + environmentService.trustedFolders = []; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + const reloaded = await initializeTestObject(); + + assert.strictEqual(true, reloaded.isWorkspaceTrusted()); + assert.strictEqual(true, (await reloaded.getUriTrustInfo(folder)).trusted); + }); + + test('an empty --trust-folder list trusts nothing', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + environmentService.trustedFolders = []; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(URI.file('/not-trusted'))); + const testObject = await initializeTestObject(); + + assert.strictEqual(false, testObject.isWorkspaceTrusted()); + assert.strictEqual(0, testObject.getTrustedUris().length); + }); + + test('a malformed --trust-folder value is ignored but valid ones are trusted', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const valid = URI.file('/valid-cli'); + // The first value has an illegal Uri scheme (URI.parse throws) and is skipped. + environmentService.trustedFolders = ['bad scheme://x', valid.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(valid)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, (await testObject.getUriTrustInfo(valid)).trusted); + assert.strictEqual(1, testObject.getTrustedUris().length); + }); + async function initializeTestObject(): Promise { const workspaceTrustManagementService = store.add(instantiationService.createInstance(WorkspaceTrustManagementService)); await workspaceTrustManagementService.workspaceTrustInitialized; From 289734cccc2fca6549a9433fdb52c887b631e704 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Sun, 2 Aug 2026 22:10:28 +0000 Subject: [PATCH 2/9] Handle --trust-folder on Windows and trust each value independently - combineUriFlags now rewrites --trust-folder so Windows does not drop the value before main.js runs. - addTrustedFoldersFromCli trusts each folder independently so one value that cannot be resolved no longer discards the other valid entries. - Adds unit tests for both. --- src/vs/code/node/cliArgs.ts | 10 +++---- src/vs/code/test/node/cliArgs.test.ts | 15 +++++++++++ .../workspaces/common/workspaceTrust.ts | 13 +++++----- .../test/common/workspaceTrust.test.ts | 26 +++++++++++++++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/vs/code/node/cliArgs.ts b/src/vs/code/node/cliArgs.ts index 2419ae6d622e2f..4db17d8bf25db9 100644 --- a/src/vs/code/node/cliArgs.ts +++ b/src/vs/code/node/cliArgs.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ /** - * Rewrites `--folder-uri ` / `--file-uri ` pairs into a single - * `--flag=value` token so the URI is not a standalone argv entry. Used on - * Windows to avoid Chromium filtering URL-like tokens before main.js runs. - * See https://github.com/microsoft/vscode/issues/209072. + * Rewrites `--folder-uri ` / `--file-uri ` / `--trust-folder ` + * pairs into a single `--flag=value` token so the URI is not a standalone argv + * entry. Used on Windows to avoid Chromium filtering URL-like tokens before + * main.js runs. See https://github.com/microsoft/vscode/issues/209072. */ export function combineUriFlags(args: string[]): string[] { const result: string[] = []; @@ -17,7 +17,7 @@ export function combineUriFlags(args: string[]): string[] { result.push(...args.slice(i)); break; } - if ((arg === '--folder-uri' || arg === '--file-uri') && i + 1 < args.length && !args[i + 1].startsWith('-')) { + if ((arg === '--folder-uri' || arg === '--file-uri' || arg === '--trust-folder') && i + 1 < args.length && !args[i + 1].startsWith('-')) { result.push(`${arg}=${args[i + 1]}`); i++; // skip the value, it's now part of the flag } else { diff --git a/src/vs/code/test/node/cliArgs.test.ts b/src/vs/code/test/node/cliArgs.test.ts index c9d7f87493d866..3715826d77662c 100644 --- a/src/vs/code/test/node/cliArgs.test.ts +++ b/src/vs/code/test/node/cliArgs.test.ts @@ -64,4 +64,19 @@ suite('combineUriFlags', () => { ] ); }); + + test('rewrites --trust-folder followed by a path or URI', () => { + assert.deepStrictEqual( + combineUriFlags([ + '--trust-folder', 'vscode-remote://ssh-remote+host/workspace', + '--trust-folder', '/local/path', + '--wait', + ]), + [ + '--trust-folder=vscode-remote://ssh-remote+host/workspace', + '--trust-folder=/local/path', + '--wait', + ] + ); + }); }); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 28fff4c9cc0216..fcf17ca7250462 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -293,20 +293,21 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork return; } - const uris: URI[] = []; for (const folder of folders) { + let uri: URI; try { // A value with a scheme (e.g. a remote `vscode-remote://` folder) is // parsed as a Uri; otherwise it is treated as a local file path. - uris.push(folder.includes('://') ? URI.parse(folder) : URI.file(folder)); + uri = folder.includes('://') ? URI.parse(folder) : URI.file(folder); } catch { - // Ignore malformed --trust-folder arguments + continue; // ignore a malformed --trust-folder value } - } - if (uris.length) { try { - await this.setUrisTrust(uris, true); + // Trust each folder independently so one value that cannot be resolved + // (e.g. a remote Uri the resolver rejects) does not discard the other, + // valid --trust-folder entries. + await this.setUrisTrust([uri], true); } catch { // Never block workspace trust initialization on a bad --trust-folder value } diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index c3ea4bf65775a3..3650c2d50f4d41 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -261,6 +261,32 @@ suite('Workspace Trust', () => { assert.strictEqual(true, (await testObject.getUriTrustInfo(remoteFolder)).trusted); }); + test('a --trust-folder value that fails to resolve does not discard the others', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const remoteAuthority = 'test+auth'; + const good = URI.file('/good-cli'); + environmentService.remoteAuthority = remoteAuthority; + environmentService.trustedFolders = [`vscode-remote://${remoteAuthority}/home/me/bad`, good.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + // The resolver is reachable but rejects canonicalization of the remote Uri, + // so trusting that one entry throws; the valid file entry must still be kept. + instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { + override async resolveAuthority(authority: string): Promise { + return { authority: { authority } } as unknown as ResolverResult; + } + override async getCanonicalURI(): Promise { + throw new Error('cannot resolve'); + } + }); + + workspaceService.setWorkspace(testWorkspace(good)); + const testObject = await initializeTestObject(); + + assert.strictEqual(true, (await testObject.getUriTrustInfo(good)).trusted); + }); + test('folders passed via --trust-folder persist across reloads', async () => { await configurationService.setUserConfiguration('security', getUserSettings(true, false)); From 6273c236a5451272c49afc2411940ea9d547b66a Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 3 Aug 2026 00:27:48 +0000 Subject: [PATCH 3/9] Use URI casing in the added comments for consistency --- src/vs/workbench/services/workspaces/common/workspaceTrust.ts | 4 ++-- .../services/workspaces/test/common/workspaceTrust.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index fcf17ca7250462..e07da67bcdd399 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -297,7 +297,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork let uri: URI; try { // A value with a scheme (e.g. a remote `vscode-remote://` folder) is - // parsed as a Uri; otherwise it is treated as a local file path. + // parsed as a URI; otherwise it is treated as a local file path. uri = folder.includes('://') ? URI.parse(folder) : URI.file(folder); } catch { continue; // ignore a malformed --trust-folder value @@ -305,7 +305,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork try { // Trust each folder independently so one value that cannot be resolved - // (e.g. a remote Uri the resolver rejects) does not discard the other, + // (e.g. a remote URI the resolver rejects) does not discard the other, // valid --trust-folder entries. await this.setUrisTrust([uri], true); } catch { diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 3650c2d50f4d41..d0bfb629f35a30 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -270,7 +270,7 @@ suite('Workspace Trust', () => { environmentService.trustedFolders = [`vscode-remote://${remoteAuthority}/home/me/bad`, good.fsPath]; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); - // The resolver is reachable but rejects canonicalization of the remote Uri, + // The resolver is reachable but rejects canonicalization of the remote URI, // so trusting that one entry throws; the valid file entry must still be kept. instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { override async resolveAuthority(authority: string): Promise { From d600549430e790761888a54ca7ceb6181e8f06ae Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Mon, 3 Aug 2026 00:38:01 +0000 Subject: [PATCH 4/9] Use URI casing in another added comment for consistency --- .../services/workspaces/test/common/workspaceTrust.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index d0bfb629f35a30..e75ab586fd9068 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -324,7 +324,7 @@ suite('Workspace Trust', () => { await configurationService.setUserConfiguration('security', getUserSettings(true, false)); const valid = URI.file('/valid-cli'); - // The first value has an illegal Uri scheme (URI.parse throws) and is skipped. + // The first value has an illegal URI scheme (URI.parse throws) and is skipped. environmentService.trustedFolders = ['bad scheme://x', valid.fsPath]; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); From 4ea8af8808cd8e843536dd7f0743e6d0d976e0ad Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Wed, 5 Aug 2026 23:59:41 +0000 Subject: [PATCH 5/9] Wait for workspace trust updates during initialization Return only after trusted URI persistence and workspace trust transitions finish, so workspaceTrustInitialized cannot resolve early. Add a regression test that stalls the transition and verifies initialization remains pending. --- .../workspaces/common/workspaceTrust.ts | 2 +- .../test/common/workspaceTrust.test.ts | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index e07da67bcdd399..56cc3ad26f9e56 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -657,7 +657,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } async setUrisTrust(uris: URI[], trusted: boolean): Promise { - this.doSetUrisTrust(await Promise.all(uris.map(uri => this.getCanonicalUri(uri))), trusted); + await this.doSetUrisTrust(await Promise.all(uris.map(uri => this.getCanonicalUri(uri))), trusted); } getTrustedUris(): URI[] { diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index e75ab586fd9068..eafa647651d374 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { promiseWithResolvers } from '../../../../../base/common/async.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -307,6 +308,35 @@ suite('Workspace Trust', () => { assert.strictEqual(true, (await reloaded.getUriTrustInfo(folder)).trusted); }); + test('workspace trust initialization waits for the --trust-folder transition', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const folder = URI.file('/trusted-transition'); + environmentService.trustedFolders = [folder.fsPath]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + workspaceService.setWorkspace(testWorkspace(folder)); + + const testObject = store.add(instantiationService.createInstance(WorkspaceTrustManagementService)); + const { promise: transitionStarted, resolve: markTransitionStarted } = promiseWithResolvers(); + const { promise: continueTransition, resolve: releaseTransition } = promiseWithResolvers(); + store.add(testObject.addWorkspaceTrustTransitionParticipant({ + async participate(): Promise { + markTransitionStarted(); + await continueTransition; + } + })); + + let initialized = false; + testObject.workspaceTrustInitialized.then(() => initialized = true); + try { + await transitionStarted; + assert.strictEqual(initialized, false); + } finally { + releaseTransition(); + await testObject.workspaceTrustInitialized; + } + }); + test('an empty --trust-folder list trusts nothing', async () => { await configurationService.setUserConfiguration('security', getUserSettings(true, false)); From c61d598d3a72bb8da57d850b17ebc16a4124bb1a Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 6 Aug 2026 01:12:08 +0000 Subject: [PATCH 6/9] Apply --trust-folder only to the launch request Copy the initial process arguments for the startup window, then remove the one-shot trust request from the shared arguments inherited by later windows. Secondary CLI launches continue to carry their own parsed arguments. Add a focused unit test for the consumption contract. --- src/vs/code/electron-main/app.ts | 4 ++-- src/vs/platform/environment/common/argv.ts | 8 ++++++++ .../platform/environment/test/node/argv.test.ts | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 60aec35c89a305..c169ffc23b5e52 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -40,7 +40,7 @@ import { ipcBrowserViewChannelName } from '../../platform/browserView/common/bro import { ipcBrowserViewGroupChannelName } from '../../platform/browserView/common/browserViewGroup.js'; import { BrowserViewMainService, IBrowserViewMainService } from '../../platform/browserView/electron-main/browserViewMainService.js'; import { BrowserViewGroupMainService, IBrowserViewGroupMainService } from '../../platform/browserView/electron-main/browserViewGroupMainService.js'; -import { NativeParsedArgs } from '../../platform/environment/common/argv.js'; +import { consumeInitialWindowArgs, NativeParsedArgs } from '../../platform/environment/common/argv.js'; import { IEnvironmentMainService } from '../../platform/environment/electron-main/environmentMainService.js'; import { isLaunchedFromCli } from '../../platform/environment/node/argvHelper.js'; import { getResolvedShellEnv } from '../../platform/shell/node/shellEnv.js'; @@ -1462,7 +1462,7 @@ export class CodeApplication extends Disposable { this.auxiliaryWindowsMainService = accessor.get(IAuxiliaryWindowsMainService); const context = isLaunchedFromCli(process.env) ? OpenContext.CLI : OpenContext.DESKTOP; - const args = this.environmentMainService.args; + const args = consumeInitialWindowArgs(this.environmentMainService.args); // Handle agents window first based on context if (args['agents']) { diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index 801537b24bb03b..a56a79bc7e8750 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -180,3 +180,11 @@ export interface NativeParsedArgs { 'trace-startup-duration'?: string; 'xdg-portal-required-version'?: string; } + +/** Copies arguments for the initial window and removes one-shot values from the shared process arguments. */ +export function consumeInitialWindowArgs(args: NativeParsedArgs): NativeParsedArgs { + const initialWindowArgs = { ...args }; + delete args['trust-folder']; + + return initialWindowArgs; +} diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index d53e61deed82d4..b6a8ecf80e1cad 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { consumeInitialWindowArgs, NativeParsedArgs } from '../../common/argv.js'; import { formatOptions, Option, OptionDescriptions, Subcommand, parseArgs, ErrorReporter } from '../../node/argv.js'; import { addArg } from '../../node/argvHelper.js'; @@ -19,6 +20,20 @@ function c(description: string, options: OptionDescriptions): Subcommand { + test('removes --trust-folder only from shared process arguments', () => { + const args: NativeParsedArgs = { _: ['/workspace'], wait: true, 'trust-folder': ['/trusted'] }; + const initialWindowArgs = consumeInitialWindowArgs(args); + + assert.deepStrictEqual({ initialWindowArgs, remainingProcessArgs: args }, { + initialWindowArgs: { _: ['/workspace'], wait: true, 'trust-folder': ['/trusted'] }, + remainingProcessArgs: { _: ['/workspace'], wait: true } + }); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); +}); + suite('formatOptions', () => { test('Text should display small columns correctly', () => { From 23ff9d3ea6ff111bf8a15bcd9961e4363fc2833d Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 6 Aug 2026 01:59:50 +0000 Subject: [PATCH 7/9] Do not reapply --trust-folder on window reload Remove one-shot window arguments from the configuration reused by Reload Window while retaining them for the initial load. Extend the focused argument test to cover both process inheritance and reload reuse. --- src/vs/platform/environment/common/argv.ts | 7 ++++++- src/vs/platform/environment/test/node/argv.test.ts | 11 +++++++---- src/vs/platform/windows/electron-main/windowImpl.ts | 3 ++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/environment/common/argv.ts b/src/vs/platform/environment/common/argv.ts index a56a79bc7e8750..4d8d66229678f2 100644 --- a/src/vs/platform/environment/common/argv.ts +++ b/src/vs/platform/environment/common/argv.ts @@ -184,7 +184,12 @@ export interface NativeParsedArgs { /** Copies arguments for the initial window and removes one-shot values from the shared process arguments. */ export function consumeInitialWindowArgs(args: NativeParsedArgs): NativeParsedArgs { const initialWindowArgs = { ...args }; - delete args['trust-folder']; + removeOneShotWindowArgs(args); return initialWindowArgs; } + +/** Removes arguments that must not be inherited by another load of a window. */ +export function removeOneShotWindowArgs(args: NativeParsedArgs): void { + delete args['trust-folder']; +} diff --git a/src/vs/platform/environment/test/node/argv.test.ts b/src/vs/platform/environment/test/node/argv.test.ts index b6a8ecf80e1cad..24c1c42b15c4d3 100644 --- a/src/vs/platform/environment/test/node/argv.test.ts +++ b/src/vs/platform/environment/test/node/argv.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { consumeInitialWindowArgs, NativeParsedArgs } from '../../common/argv.js'; +import { consumeInitialWindowArgs, NativeParsedArgs, removeOneShotWindowArgs } from '../../common/argv.js'; import { formatOptions, Option, OptionDescriptions, Subcommand, parseArgs, ErrorReporter } from '../../node/argv.js'; import { addArg } from '../../node/argvHelper.js'; @@ -21,13 +21,16 @@ function c(description: string, options: OptionDescriptions): Subcommand { - test('removes --trust-folder only from shared process arguments', () => { + test('removes --trust-folder from shared process and reload arguments', () => { const args: NativeParsedArgs = { _: ['/workspace'], wait: true, 'trust-folder': ['/trusted'] }; const initialWindowArgs = consumeInitialWindowArgs(args); + const reloadArgs = { ...initialWindowArgs }; + removeOneShotWindowArgs(reloadArgs); - assert.deepStrictEqual({ initialWindowArgs, remainingProcessArgs: args }, { + assert.deepStrictEqual({ initialWindowArgs, remainingProcessArgs: args, reloadArgs }, { initialWindowArgs: { _: ['/workspace'], wait: true, 'trust-folder': ['/trusted'] }, - remainingProcessArgs: { _: ['/workspace'], wait: true } + remainingProcessArgs: { _: ['/workspace'], wait: true }, + reloadArgs: { _: ['/workspace'], wait: true } }); }); diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 4a51cc55ba09b9..52f130d80ce883 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -19,7 +19,7 @@ import { ISerializableCommandAction } from '../../action/common/action.js'; import { IBackupMainService } from '../../backup/electron-main/backup.js'; import { IConfigurationChangeEvent, IConfigurationService } from '../../configuration/common/configuration.js'; import { IDialogMainService } from '../../dialogs/electron-main/dialogMainService.js'; -import { NativeParsedArgs } from '../../environment/common/argv.js'; +import { NativeParsedArgs, removeOneShotWindowArgs } from '../../environment/common/argv.js'; import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js'; import { isLaunchedFromCli } from '../../environment/node/argvHelper.js'; import { IFileService } from '../../files/common/files.js'; @@ -1307,6 +1307,7 @@ export class CodeWindow extends BaseWindow implements ICodeWindow { delete configuration.filesToDiff; delete configuration.filesToMerge; delete configuration.filesToWait; + removeOneShotWindowArgs(configuration); // Some configuration things get inherited if the window is being reloaded and we are // in extension development mode. These options are all development related. From 463cf8593746e6bd04d475ce5847ded334c704bd Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Thu, 20 Aug 2026 11:04:23 -0700 Subject: [PATCH 8/9] Condense Workspace Trust inline comments Apply the current one-line inline-comment convention to the trust-folder ordering and test rationale comments without changing behavior. --- .../services/workspaces/common/workspaceTrust.ts | 13 +++---------- .../workspaces/test/common/workspaceTrust.test.ts | 9 +++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 56cc3ad26f9e56..395cc383f9a5f6 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -156,8 +156,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork this._workspaceResolvedPromiseResolve(); if (!this.environmentService.remoteAuthority) { - // Persist folders passed via `--trust-folder` before signalling that - // trust is initialized, so they are already trusted at startup. + // Apply `--trust-folder` before signalling workspace trust initialization. this.addTrustedFoldersFromCli().finally(() => this._workspaceTrustInitializedPromiseResolve()); } }); @@ -171,9 +170,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork await this.updateWorkspaceTrust(); }) .finally(() => { - // The remote authority is now resolved, so `--trust-folder` values - // for remote folders can be canonicalized before signalling that - // trust is initialized. + // Apply remote `--trust-folder` values after resolver canonicalization is available. this.addTrustedFoldersFromCli().finally(() => this._workspaceTrustInitializedPromiseResolve()); }); } @@ -296,17 +293,13 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork for (const folder of folders) { let uri: URI; try { - // A value with a scheme (e.g. a remote `vscode-remote://` folder) is - // parsed as a URI; otherwise it is treated as a local file path. uri = folder.includes('://') ? URI.parse(folder) : URI.file(folder); } catch { continue; // ignore a malformed --trust-folder value } try { - // Trust each folder independently so one value that cannot be resolved - // (e.g. a remote URI the resolver rejects) does not discard the other, - // valid --trust-folder entries. + // Isolate resolution failures so valid `--trust-folder` entries are still applied. await this.setUrisTrust([uri], true); } catch { // Never block workspace trust initialization on a bad --trust-folder value diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index 409d410b0daac9..ce340437f120dd 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -244,8 +244,7 @@ suite('Workspace Trust', () => { environmentService.trustedFolders = [`vscode-remote://${remoteAuthority}/home/me/proj`]; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); - // A resolver that is reachable but does not mark the remote as trusted, so - // trust must come from the --trust-folder entry rather than the remote itself. + // Trust must come from `--trust-folder` rather than the remote resolver. instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { override async resolveAuthority(authority: string): Promise { return { authority: { authority } } as unknown as ResolverResult; @@ -271,8 +270,7 @@ suite('Workspace Trust', () => { environmentService.trustedFolders = [`vscode-remote://${remoteAuthority}/home/me/bad`, good.fsPath]; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); - // The resolver is reachable but rejects canonicalization of the remote URI, - // so trusting that one entry throws; the valid file entry must still be kept. + // A rejected remote URI must not discard the valid file entry. instantiationService.stub(IRemoteAuthorityResolverService, new class extends mock() { override async resolveAuthority(authority: string): Promise { return { authority: { authority } } as unknown as ResolverResult; @@ -298,8 +296,7 @@ suite('Workspace Trust', () => { workspaceService.setWorkspace(testWorkspace(folder)); await initializeTestObject(); - // A subsequent window without the flag still trusts the folder because - // it was persisted to the trusted folder list. + // The persisted entry must survive a launch without the flag. environmentService.trustedFolders = []; instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); const reloaded = await initializeTestObject(); From 0dd1737a33b10259c9f48130a50fd4ca44371c96 Mon Sep 17 00:00:00 2001 From: Sean McManus Date: Sat, 22 Aug 2026 18:02:34 -0700 Subject: [PATCH 9/9] Normalize trailing separators in --trust-folder Remove trailing path separators before persisting CLI trust entries so a tab-completed directory path trusts the directory itself. Add a cross-platform native-path regression test. --- .../workspaces/common/workspaceTrust.ts | 1 + .../test/common/workspaceTrust.test.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 395cc383f9a5f6..02cf0cfdfca464 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -294,6 +294,7 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork let uri: URI; try { uri = folder.includes('://') ? URI.parse(folder) : URI.file(folder); + uri = this.uriIdentityService.extUri.removeTrailingPathSeparator(uri); } catch { continue; // ignore a malformed --trust-folder value } diff --git a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts index ce340437f120dd..a0f55e4200f454 100644 --- a/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts +++ b/src/vs/workbench/services/workspaces/test/common/workspaceTrust.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { promiseWithResolvers } from '../../../../../base/common/async.js'; +import { sep } from '../../../../../base/common/path.js'; import { URI } from '../../../../../base/common/uri.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; @@ -189,6 +190,25 @@ suite('Workspace Trust', () => { assert.strictEqual(true, testObject.isWorkspaceTrusted()); }); + test('trust folder passed via --trust-folder with a trailing separator', async () => { + await configurationService.setUserConfiguration('security', getUserSettings(true, false)); + + const folder = URI.file('/trusted-trailing-separator'); + environmentService.trustedFolders = [`${folder.fsPath}${sep}`]; + instantiationService.stub(IWorkbenchEnvironmentService, { ...environmentService }); + + workspaceService.setWorkspace(testWorkspace(folder)); + const testObject = await initializeTestObject(); + + assert.deepStrictEqual({ + workspaceTrusted: testObject.isWorkspaceTrusted(), + trustedUris: testObject.getTrustedUris().map(uri => uri.toString()) + }, { + workspaceTrusted: true, + trustedUris: [folder.toString()] + }); + }); + test('trust multiple folders passed via --trust-folder', async () => { await configurationService.setUserConfiguration('security', getUserSettings(true, false));