From 7d99ced3b1d1e2c550eeb2bc121825c694ccdfc2 Mon Sep 17 00:00:00 2001 From: dvd233 <111864431+dvd233@users.noreply.github.com> Date: Tue, 22 Sep 2026 12:32:43 -0700 Subject: [PATCH] fix(agent): detect case-insensitive Windows configuration collisions --- src/agent/configurator.ts | 12 ++++++++++-- test/agent/configurator.test.ts | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/agent/configurator.ts b/src/agent/configurator.ts index f5675613..28ba72f2 100644 --- a/src/agent/configurator.ts +++ b/src/agent/configurator.ts @@ -990,6 +990,13 @@ function writeTarget(path: string): string { } } +export function configurationTargetIdentity( + targetPath: string, + platform: NodeJS.Platform = process.platform, +): string { + return platform === 'win32' ? targetPath.toLowerCase() : targetPath; +} + function hasWeakPermissions(mode: number): boolean { return process.platform !== 'win32' && (mode & 0o077) !== 0; } @@ -997,14 +1004,15 @@ function hasWeakPermissions(mode: number): boolean { function assertDistinctConfigurationTargets(prepared: PreparedAgentFile[]): void { const targets = new Set(); for (const file of prepared) { - if (targets.has(file.targetPath)) { + const identity = configurationTargetIdentity(file.targetPath); + if (targets.has(identity)) { throw new CLIError( `Multiple agent configuration paths resolve to ${file.targetPath}.`, ExitCode.GENERAL, 'No files were changed. Use distinct configuration paths and retry.', ); } - targets.add(file.targetPath); + targets.add(identity); } } diff --git a/test/agent/configurator.test.ts b/test/agent/configurator.test.ts index 8aaf788e..291db3eb 100644 --- a/test/agent/configurator.test.ts +++ b/test/agent/configurator.test.ts @@ -21,6 +21,7 @@ import { parse as parseYaml } from 'yaml'; import { applyAgentConfigurations, + configurationTargetIdentity, prepareAgentConfigurations, } from '../../src/agent/configurator'; import { AGENT_IDS, type AgentId, type AgentSetupOptions } from '../../src/agent/types'; @@ -462,6 +463,26 @@ describe('agent configurator', () => { expect(existsSync(shared)).toBe(false); }); + it('compares configuration target paths case-insensitively on Windows', () => { + expect(configurationTargetIdentity('C:\\Users\\Alice\\settings.json', 'win32')) + .toBe('c:\\users\\alice\\settings.json'); + expect(configurationTargetIdentity('/Users/Alice/settings.json', 'linux')) + .toBe('/Users/Alice/settings.json'); + }); + + it('rejects duplicate targets that differ only by case on Windows', () => { + if (process.platform !== 'win32') return; + + const shared = join(home, 'shared-agent-config'); + mkdirSync(shared, { recursive: true }); + const differentlyCased = shared.replace('shared-agent-config', 'SHARED-AGENT-CONFIG'); + + expect(() => prepareAgentConfigurations(setupOptions( + ['claude-code', 'pi'], + { env: { CLAUDE_CONFIG_DIR: shared, PI_CODING_AGENT_DIR: differentlyCased } }, + ))).toThrow('Multiple agent configuration paths'); + }); + it('removes the setup lock when installation receives an exit signal', async () => { if (process.platform === 'win32') return; const configuratorUrl = pathToFileURL(