Skip to content

Commit d057f3f

Browse files
authored
Fix hook cwd resolution in multi-root workspaces (microsoft#295365)
Previously, computeHooks() and getHookDiscoveryInfo() used folders[0] for the workspaceRootUri of all hook files, causing hooks in non-first workspace folders to have incorrect cwd. Now each hook file's cwd is resolved using getWorkspaceFolder() for that specific hook file's URI, ensuring hooks run in the correct workspace folder. Adds a test that verifies multi-root cwd resolution.
1 parent c5cf29b commit d057f3f

File tree

2 files changed

+67
-9
lines changed

2 files changed

+67
-9
lines changed

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,10 +1026,6 @@ export class PromptsService extends Disposable implements IPromptsService {
10261026
const userHomeUri = await this.pathService.userHome();
10271027
const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path;
10281028

1029-
// Get workspace root for resolving relative cwd paths
1030-
const workspaceFolder = this.workspaceService.getWorkspace().folders[0];
1031-
const workspaceRootUri = workspaceFolder?.uri;
1032-
10331029
let hasDisabledClaudeHooks = false;
10341030
const collectedHooks: Record<HookType, IHookCommand[]> = {
10351031
[HookType.SessionStart]: [],
@@ -1042,11 +1038,18 @@ export class PromptsService extends Disposable implements IPromptsService {
10421038
[HookType.Stop]: [],
10431039
};
10441040

1041+
const defaultFolder = this.workspaceService.getWorkspace().folders[0];
1042+
10451043
for (const hookFile of hookFiles) {
10461044
try {
10471045
const content = await this.fileService.readFile(hookFile.uri);
10481046
const json = parseJSONC(content.value.toString());
10491047

1048+
// Resolve the workspace folder that contains this hook file for cwd resolution,
1049+
// falling back to the first workspace folder for user-level hooks outside the workspace
1050+
const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(hookFile.uri) ?? defaultFolder;
1051+
const workspaceRootUri = hookWorkspaceFolder?.uri;
1052+
10501053
// Use format-aware parsing that handles Copilot and Claude formats
10511054
const { format, hooks, disabledAllHooks } = parseHooksFromFile(hookFile.uri, json, workspaceRootUri, userHome);
10521055

@@ -1339,10 +1342,6 @@ export class PromptsService extends Disposable implements IPromptsService {
13391342
const userHomeUri = await this.pathService.userHome();
13401343
const userHome = userHomeUri.scheme === Schemas.file ? userHomeUri.fsPath : userHomeUri.path;
13411344

1342-
// Get workspace root for resolving relative cwd paths
1343-
const workspaceFolder = this.workspaceService.getWorkspace().folders[0];
1344-
const workspaceRootUri = workspaceFolder?.uri;
1345-
13461345
const useClaudeHooks = this.configurationService.getValue<boolean>(PromptsConfig.USE_CLAUDE_HOOKS);
13471346
const hookFiles = await this.listPromptFiles(PromptsType.hook, token);
13481347
for (const promptPath of hookFiles) {
@@ -1383,6 +1382,11 @@ export class PromptsService extends Disposable implements IPromptsService {
13831382
continue;
13841383
}
13851384

1385+
// Resolve the workspace folder that contains this hook file for cwd resolution,
1386+
// falling back to the first workspace folder for user-level hooks outside the workspace
1387+
const hookWorkspaceFolder = this.workspaceService.getWorkspaceFolder(uri) ?? this.workspaceService.getWorkspace().folders[0];
1388+
const workspaceRootUri = hookWorkspaceFolder?.uri;
1389+
13861390
// Use format-aware parsing to check for disabledAllHooks
13871391
const { disabledAllHooks } = parseHooksFromFile(uri, json, workspaceRootUri, userHome);
13881392

src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { TestContextService, TestUserDataProfileService } from '../../../../../.
3838
import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/attachments/chatVariableEntries.js';
3939
import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js';
4040
import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js';
41-
import { AGENTS_SOURCE_FOLDER, CLAUDE_CONFIG_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js';
41+
import { AGENTS_SOURCE_FOLDER, CLAUDE_CONFIG_FOLDER, HOOKS_SOURCE_FOLDER, INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js';
4242
import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js';
4343
import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage, Target } from '../../../../common/promptSyntax/service/promptsService.js';
4444
import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js';
@@ -48,6 +48,7 @@ import { IPathService } from '../../../../../../services/path/common/pathService
4848
import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js';
4949
import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js';
5050
import { ChatModeKind } from '../../../../common/constants.js';
51+
import { HookType } from '../../../../common/promptSyntax/hookSchema.js';
5152

5253
suite('PromptsService', () => {
5354
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
@@ -3370,4 +3371,57 @@ suite('PromptsService', () => {
33703371
'Skill without header should be included when applying userInvocable filter (defaults to true)');
33713372
});
33723373
});
3374+
3375+
suite('hooks', () => {
3376+
test('multi-root workspace resolves cwd to per-hook-file workspace folder', async function () {
3377+
const folder1Uri = URI.file('/workspace-a');
3378+
const folder2Uri = URI.file('/workspace-b');
3379+
3380+
workspaceContextService.setWorkspace(testWorkspace(folder1Uri, folder2Uri));
3381+
testConfigService.setUserConfiguration(PromptsConfig.USE_CHAT_HOOKS, true);
3382+
testConfigService.setUserConfiguration(PromptsConfig.HOOKS_LOCATION_KEY, { [HOOKS_SOURCE_FOLDER]: true });
3383+
3384+
await mockFiles(fileService, [
3385+
{
3386+
path: '/workspace-a/.github/hooks/my-hook.json',
3387+
contents: [
3388+
JSON.stringify({
3389+
hooks: {
3390+
[HookType.PreToolUse]: [
3391+
{ type: 'command', command: 'echo folder-a' },
3392+
],
3393+
},
3394+
}),
3395+
],
3396+
},
3397+
{
3398+
path: '/workspace-b/.github/hooks/my-hook.json',
3399+
contents: [
3400+
JSON.stringify({
3401+
hooks: {
3402+
[HookType.PreToolUse]: [
3403+
{ type: 'command', command: 'echo folder-b' },
3404+
],
3405+
},
3406+
}),
3407+
],
3408+
},
3409+
]);
3410+
3411+
const result = await service.getHooks(CancellationToken.None);
3412+
assert.ok(result, 'Expected hooks result');
3413+
3414+
const preToolUseHooks = result.hooks[HookType.PreToolUse];
3415+
assert.ok(preToolUseHooks, 'Expected PreToolUse hooks');
3416+
assert.strictEqual(preToolUseHooks.length, 2, 'Expected two PreToolUse hooks');
3417+
3418+
const hookA = preToolUseHooks.find(h => h.command === 'echo folder-a');
3419+
const hookB = preToolUseHooks.find(h => h.command === 'echo folder-b');
3420+
assert.ok(hookA, 'Expected hook from folder-a');
3421+
assert.ok(hookB, 'Expected hook from folder-b');
3422+
3423+
assert.strictEqual(hookA.cwd?.path, folder1Uri.path, 'Hook from folder-a should have cwd pointing to workspace-a');
3424+
assert.strictEqual(hookB.cwd?.path, folder2Uri.path, 'Hook from folder-b should have cwd pointing to workspace-b');
3425+
});
3426+
});
33733427
});

0 commit comments

Comments
 (0)