diff --git a/lib/hooks/image-analyzer-transformer.cjs b/lib/hooks/image-analyzer-transformer.cjs index 09362bb6..b041dd17 100755 --- a/lib/hooks/image-analyzer-transformer.cjs +++ b/lib/hooks/image-analyzer-transformer.cjs @@ -761,14 +761,16 @@ process.stdin.on('error', () => { /** * Main hook processing logic + * + * Two-phase design: Phase 1 filters non-image Read calls silently (exit 0). + * Phase 2 only runs for confirmed image/PDF files, so error messages are + * always relevant and never confuse users reading code or text files. */ async function processHook() { + // Phase 1: Fast bail-out for non-image files + // Any failure here → pass through silently to native Read + let filePath; try { - // Skip for native accounts or explicit disable - if (shouldSkipHook()) { - process.exit(0); - } - const data = JSON.parse(input); // Only handle Read tool @@ -776,23 +778,35 @@ async function processHook() { process.exit(0); } - const filePath = data.tool_input?.file_path || ''; + filePath = data.tool_input?.file_path || ''; if (!filePath) { process.exit(0); } + // Check file extension BEFORE any other processing — this is the key gate + // that ensures non-image Read calls never see hook errors + if (!isAnalyzableFile(filePath)) { + process.exit(0); + } + } catch { + // stdin parse failure or unexpected error → pass through silently + process.exit(0); + } + + // Phase 2: Image/PDF file processing — errors here are relevant to the user + try { + // Skip for native accounts or explicit disable + if (shouldSkipHook()) { + process.exit(0); + } + // Check if file exists if (!fs.existsSync(filePath)) { // Let native Read handle the error process.exit(0); } - // Check if file is analyzable - if (!isAnalyzableFile(filePath)) { - process.exit(0); - } - // Check file size const stats = fs.statSync(filePath); if (stats.size >= MAX_FILE_SIZE_BYTES) { @@ -843,14 +857,7 @@ async function processHook() { console.error('[CCS Hook] Error:', err.message); } - // Try to extract file path from parsed input - let filePath = 'unknown file'; - try { - const data = JSON.parse(input); - filePath = data.tool_input?.file_path || 'unknown file'; - } catch { - // Ignore parse errors - } + // filePath is guaranteed set by Phase 1 — only image files reach here // Categorize error by message pattern const errMsg = err.message || ''; diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 986868fb..807ca8b1 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -50,6 +50,7 @@ import { displayWebSearchStatus, } from '../../utils/websearch-manager'; import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; +import { installImageAnalyzerHook } from '../../utils/hooks'; import { HttpsTunnelProxy } from '../https-tunnel-proxy'; // Import modular components @@ -162,6 +163,9 @@ export async function execClaudeWithCLIProxy( installWebSearchHook(); displayWebSearchStatus(); + // Sync image analyzer hook from npm package to ~/.ccs/hooks/ + installImageAnalyzerHook(); + const providerConfig = getProviderConfig(provider); log(`Provider: ${providerConfig.displayName}`); diff --git a/tests/e2e/image-analyzer-hook.e2e.test.ts b/tests/e2e/image-analyzer-hook.e2e.test.ts index de00aa62..8df3adfa 100644 --- a/tests/e2e/image-analyzer-hook.e2e.test.ts +++ b/tests/e2e/image-analyzer-hook.e2e.test.ts @@ -425,8 +425,8 @@ describe('Image Analyzer Hook', () => { }, }); - // Should exit with error (code 2) - expect(hookProcess.status).toBe(2); + // Should pass through silently (exit 0) — can't determine file type from malformed input + expect(hookProcess.status).toBe(0); }); });