fix(websearch): add type guards and deduplication for global settings

- Add typeof check before .replace() to prevent crash on non-string commands
- Add isCcsWebSearchHook() and deduplicateCcsHooks() to hook-config.ts
- Fix ensureHookConfig() detection to verify path, not just matcher
- Prevent overwriting user's custom WebSearch hooks
This commit is contained in:
kaitranntt
2026-02-02 11:36:48 -05:00
parent d61c940a08
commit 847aad00fe
2 changed files with 71 additions and 5 deletions
+65 -2
View File
@@ -51,6 +51,47 @@ export function getHookPath(): string {
return path.join(getCcsHooksDir(), WEBSEARCH_HOOK);
}
/**
* Check if a hook entry is a CCS WebSearch hook
*/
function isCcsWebSearchHook(hook: Record<string, unknown>): boolean {
if (hook.matcher !== 'WebSearch') return false;
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
if (!hookArray?.[0]?.command) return false;
const command = hookArray[0].command;
if (typeof command !== 'string') return false;
// 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<string, unknown>): boolean {
const hooks = settings.hooks as Record<string, unknown[]> | 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<string, unknown>;
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;
}
/**
* Get WebSearch hook configuration for settings.json
* Timeout is computed from max provider timeout in config.yaml + buffer
@@ -123,11 +164,27 @@ export function ensureHookConfig(): boolean {
if (hooks?.PreToolUse) {
const webSearchHookIndex = hooks.PreToolUse.findIndex((h: unknown) => {
const hook = h as Record<string, unknown>;
return hook.matcher === 'WebSearch';
if (hook.matcher !== 'WebSearch') return false;
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
const command = hookArray?.[0]?.command;
if (typeof command !== 'string') return false;
const normalized = command.replace(/\\/g, '/');
return normalized.includes('.ccs/hooks/websearch-transformer');
});
if (webSearchHookIndex !== -1) {
// Hook exists - check if it needs updating
// Hook exists - first clean up any duplicates
const hadDuplicates = deduplicateCcsHooks(settings);
if (hadDuplicates) {
fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) {
console.error(info('Removed duplicate WebSearch hooks from settings.json'));
}
}
// Then check if it needs updating
const existingHook = hooks.PreToolUse[webSearchHookIndex] as Record<string, unknown>;
const existingHooks = existingHook.hooks as Array<Record<string, unknown>>;
const currentHookConfig = getWebSearchHookConfig();
@@ -170,6 +227,12 @@ export function ensureHookConfig(): boolean {
settingsHooks.PreToolUse = [];
}
// Remove any existing CCS hooks first to prevent duplicates
settingsHooks.PreToolUse = settingsHooks.PreToolUse.filter((h: unknown) => {
const hook = h as Record<string, unknown>;
return !isCcsWebSearchHook(hook);
});
// Add our hook config
const preToolUseHooks = hookConfig.PreToolUse as unknown[];
settingsHooks.PreToolUse.push(...preToolUseHooks);
+6 -3
View File
@@ -39,7 +39,8 @@ function hasCcsHook(settings: Record<string, unknown>): boolean {
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
if (!hookArray?.[0]?.command) return false;
const command = hookArray[0].command as string;
const command = hookArray[0].command;
if (typeof command !== 'string') return false;
// Normalize path separators for cross-platform matching (Windows uses backslashes)
const normalizedCommand = command.replace(/\\/g, '/');
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
@@ -55,7 +56,8 @@ function isCcsWebSearchHook(hook: Record<string, unknown>): boolean {
const hookArray = hook.hooks as Array<Record<string, unknown>> | undefined;
if (!hookArray?.[0]?.command) return false;
const command = hookArray[0].command as string;
const command = hookArray[0].command;
if (typeof command !== 'string') return false;
// Normalize path separators for cross-platform matching (Windows uses backslashes)
const normalizedCommand = command.replace(/\\/g, '/');
return normalizedCommand.includes('.ccs/hooks/websearch-transformer');
@@ -240,7 +242,8 @@ function updateHookTimeoutIfNeeded(
const hookArray = hook.hooks as Array<Record<string, unknown>>;
if (!hookArray?.[0]?.command) continue;
const command = hookArray[0].command as string;
const command = hookArray[0].command;
if (typeof command !== 'string') continue;
// Normalize path separators for cross-platform matching (Windows uses backslashes)
const normalizedCommand = command.replace(/\\/g, '/');
if (!normalizedCommand.includes('.ccs/hooks/websearch-transformer')) continue;