fix(websearch): stop polluting global ~/.claude/settings.json with hooks

Remove global hook registration from installWebSearchHook() - now only
copies hook file to ~/.ccs/hooks/. Hook registration handled by
ensureProfileHooks() which writes to per-profile settings.

Changes:
- hook-installer.ts: Remove ensureHookConfig() call
- ccs.ts: Remove redundant installWebSearchHook() call
- hook-config.ts: Add getClaudeSettingsPath() for test isolation
This commit is contained in:
kaitranntt
2026-01-26 14:51:51 -05:00
parent 60e7441246
commit 0216341b2c
3 changed files with 27 additions and 16 deletions
-2
View File
@@ -8,7 +8,6 @@ import { ErrorManager } from './utils/error-manager';
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy'; import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
import { import {
ensureMcpWebSearch, ensureMcpWebSearch,
installWebSearchHook,
displayWebSearchStatus, displayWebSearchStatus,
getWebSearchHookEnv, getWebSearchHookEnv,
ensureProfileHooks, ensureProfileHooks,
@@ -549,7 +548,6 @@ async function main(): Promise<void> {
ensureProfileHooks(profileInfo.name); ensureProfileHooks(profileInfo.name);
ensureMcpWebSearch(); ensureMcpWebSearch();
installWebSearchHook();
// Display WebSearch status (single line, equilibrium UX) // Display WebSearch status (single line, equilibrium UX)
displayWebSearchStatus(); displayWebSearchStatus();
+23 -11
View File
@@ -13,12 +13,24 @@ import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader'; import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getCcsDir } from '../config-manager'; import { getCcsDir } from '../config-manager';
// Path to Claude settings.json (intentionally uses real homedir for global settings)
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
// Hook file name // Hook file name
const WEBSEARCH_HOOK = 'websearch-transformer.cjs'; const WEBSEARCH_HOOK = 'websearch-transformer.cjs';
/**
* Get Claude settings path (respects CCS_HOME for test isolation)
* In tests, returns path under CCS_HOME; in production, uses real ~/.claude/
*/
function getClaudeSettingsPath(): string {
const ccsHome = process.env.CCS_HOME;
if (ccsHome) {
// Test mode: use CCS_HOME parent for .claude directory
// This prevents tests from modifying user's real settings
return path.join(path.dirname(ccsHome), '.claude', 'settings.json');
}
// Production: use real home directory
return path.join(os.homedir(), '.claude', 'settings.json');
}
/** /**
* Get CCS hooks directory (respects CCS_HOME for test isolation) * Get CCS hooks directory (respects CCS_HOME for test isolation)
*/ */
@@ -92,9 +104,9 @@ export function ensureHookConfig(): boolean {
// Read existing settings or start fresh // Read existing settings or start fresh
let settings: Record<string, unknown> = {}; let settings: Record<string, unknown> = {};
if (fs.existsSync(CLAUDE_SETTINGS_PATH)) { if (fs.existsSync(getClaudeSettingsPath())) {
try { try {
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'); const content = fs.readFileSync(getClaudeSettingsPath(), 'utf8');
settings = JSON.parse(content); settings = JSON.parse(content);
} catch { } catch {
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
@@ -136,7 +148,7 @@ export function ensureHookConfig(): boolean {
} }
if (needsUpdate) { if (needsUpdate) {
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8'); fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error(info('Updated WebSearch hook config in settings.json')); console.error(info('Updated WebSearch hook config in settings.json'));
} }
@@ -163,13 +175,13 @@ export function ensureHookConfig(): boolean {
settingsHooks.PreToolUse.push(...preToolUseHooks); settingsHooks.PreToolUse.push(...preToolUseHooks);
// Ensure ~/.claude directory exists // Ensure ~/.claude directory exists
const claudeDir = path.dirname(CLAUDE_SETTINGS_PATH); const claudeDir = path.dirname(getClaudeSettingsPath());
if (!fs.existsSync(claudeDir)) { if (!fs.existsSync(claudeDir)) {
fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 }); fs.mkdirSync(claudeDir, { recursive: true, mode: 0o700 });
} }
// Write updated settings // Write updated settings
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8'); fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error(info('Added WebSearch hook to settings.json')); console.error(info('Added WebSearch hook to settings.json'));
@@ -191,11 +203,11 @@ export function ensureHookConfig(): boolean {
*/ */
export function removeHookConfig(): boolean { export function removeHookConfig(): boolean {
try { try {
if (!fs.existsSync(CLAUDE_SETTINGS_PATH)) { if (!fs.existsSync(getClaudeSettingsPath())) {
return true; // Nothing to remove return true; // Nothing to remove
} }
const content = fs.readFileSync(CLAUDE_SETTINGS_PATH, 'utf8'); const content = fs.readFileSync(getClaudeSettingsPath(), 'utf8');
let settings: Record<string, unknown>; let settings: Record<string, unknown>;
try { try {
settings = JSON.parse(content); settings = JSON.parse(content);
@@ -232,7 +244,7 @@ export function removeHookConfig(): boolean {
delete settings.hooks; delete settings.hooks;
} }
fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8'); fs.writeFileSync(getClaudeSettingsPath(), JSON.stringify(settings, null, 2), 'utf8');
if (process.env.CCS_DEBUG) { if (process.env.CCS_DEBUG) {
console.error(info('Removed WebSearch hook from settings.json')); console.error(info('Removed WebSearch hook from settings.json'));
+4 -3
View File
@@ -10,7 +10,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { info, warn } from '../ui'; import { info, warn } from '../ui';
import { getWebSearchConfig } from '../../config/unified-config-loader'; import { getWebSearchConfig } from '../../config/unified-config-loader';
import { getHookPath, ensureHookConfig, getCcsHooksDir } from './hook-config'; import { getHookPath, getCcsHooksDir } from './hook-config';
import { removeMigrationMarker } from './profile-hook-injector'; import { removeMigrationMarker } from './profile-hook-injector';
// Re-export from hook-config for backward compatibility // Re-export from hook-config for backward compatibility
@@ -85,8 +85,9 @@ export function installWebSearchHook(): boolean {
console.error(info(`Installed WebSearch hook: ${hookPath}`)); console.error(info(`Installed WebSearch hook: ${hookPath}`));
} }
// Ensure hook is configured in settings.json // Note: Hook registration is handled by ensureProfileHooks() in profile-hook-injector.ts
ensureHookConfig(); // which writes to per-profile settings (~/.ccs/<profile>.settings.json)
// Global settings (~/.claude/settings.json) are NOT modified here
return true; return true;
} catch (error) { } catch (error) {