From d61c940a087d4e9134fa0a9ae32dc8d79d42648d Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 2 Feb 2026 11:18:20 -0500 Subject: [PATCH] fix(websearch): normalize Windows path separators in hook detection On Windows, path.join() produces backslash paths (C:\Users\.ccs\hooks\...) but detection used forward slashes, causing: - Failed hook detection on Windows - Duplicate hooks added on every CCS invocation Changes: - Normalize path separators (\ -> /) before matching - Add deduplicateCcsHooks() to auto-cleanup accumulated duplicates - Fix affects hasCcsHook(), isCcsWebSearchHook(), removeHookConfig() --- src/utils/websearch/hook-config.ts | 4 +- src/utils/websearch/profile-hook-injector.ts | 58 +++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/utils/websearch/hook-config.ts b/src/utils/websearch/hook-config.ts index d05003c0..21a0c765 100644 --- a/src/utils/websearch/hook-config.ts +++ b/src/utils/websearch/hook-config.ts @@ -229,7 +229,9 @@ export function removeHookConfig(): boolean { if (!hookArray?.[0]?.command) return true; // Keep malformed entries const command = hookArray[0].command as string; - return !command.includes('.ccs/hooks/websearch-transformer'); // Remove if CCS hook + // Normalize path separators for cross-platform matching (Windows uses backslashes) + const normalizedCommand = command.replace(/\\/g, '/'); + return !normalizedCommand.includes('.ccs/hooks/websearch-transformer'); // Remove if CCS hook }); if (hooks.PreToolUse.length === originalLength) { diff --git a/src/utils/websearch/profile-hook-injector.ts b/src/utils/websearch/profile-hook-injector.ts index 214184da..56a9c477 100644 --- a/src/utils/websearch/profile-hook-injector.ts +++ b/src/utils/websearch/profile-hook-injector.ts @@ -40,10 +40,52 @@ function hasCcsHook(settings: Record): boolean { if (!hookArray?.[0]?.command) return false; const command = hookArray[0].command as string; - return command.includes('.ccs/hooks/websearch-transformer'); + // Normalize path separators for cross-platform matching (Windows uses backslashes) + const normalizedCommand = command.replace(/\\/g, '/'); + return normalizedCommand.includes('.ccs/hooks/websearch-transformer'); }); } +/** + * Check if a hook entry is a CCS WebSearch hook + */ +function isCcsWebSearchHook(hook: Record): boolean { + if (hook.matcher !== 'WebSearch') return false; + + const hookArray = hook.hooks as Array> | undefined; + if (!hookArray?.[0]?.command) return false; + + const command = hookArray[0].command as string; + // Normalize path separators for cross-platform matching (Windows uses backslashes) + const normalizedCommand = command.replace(/\\/g, '/'); + return normalizedCommand.includes('.ccs/hooks/websearch-transformer'); +} + +/** + * Remove duplicate CCS WebSearch hooks from settings, keeping only the first one + * Returns true if duplicates were removed + */ +function deduplicateCcsHooks(settings: Record): boolean { + const hooks = settings.hooks as Record | undefined; + if (!hooks?.PreToolUse) return false; + + let foundFirst = false; + const originalLength = hooks.PreToolUse.length; + + hooks.PreToolUse = hooks.PreToolUse.filter((h: unknown) => { + const hook = h as Record; + if (!isCcsWebSearchHook(hook)) return true; // Keep non-CCS hooks + + if (!foundFirst) { + foundFirst = true; + return true; // Keep first CCS hook + } + return false; // Remove subsequent duplicates + }); + + return hooks.PreToolUse.length < originalLength; +} + /** * Migrate CCS hook from global settings to profile settings (one-time) */ @@ -126,6 +168,16 @@ export function ensureProfileHooks(profileName: string): boolean { // Check if CCS hook already present if (hasCcsHook(settings)) { + // Clean up any duplicates that may have accumulated (Windows path bug fix) + const hadDuplicates = deduplicateCcsHooks(settings); + if (hadDuplicates) { + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8'); + if (process.env.CCS_DEBUG) { + console.error( + info(`Removed duplicate WebSearch hooks from ${profileName}.settings.json`) + ); + } + } // Update timeout if needed return updateHookTimeoutIfNeeded(settings, settingsPath); } @@ -189,7 +241,9 @@ function updateHookTimeoutIfNeeded( if (!hookArray?.[0]?.command) continue; const command = hookArray[0].command as string; - if (!command.includes('.ccs/hooks/websearch-transformer')) continue; + // Normalize path separators for cross-platform matching (Windows uses backslashes) + const normalizedCommand = command.replace(/\\/g, '/'); + if (!normalizedCommand.includes('.ccs/hooks/websearch-transformer')) continue; // Found CCS hook - check if needs update if (hookArray[0].command !== expectedCommand) {