diff --git a/lib/hooks/block-image-read.cjs b/lib/hooks/block-image-read.cjs deleted file mode 100644 index 78d87e27..00000000 --- a/lib/hooks/block-image-read.cjs +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env node -/** - * CCS Image Read Blocking Hook - * - * Blocks Claude's Read tool from reading image files to prevent context overflow. - * Each image can consume 100K+ tokens, causing immediate context exhaustion. - * - * This is a PreToolUse hook that runs BEFORE the tool is executed. - * - * Behavior (matches WebSearch pattern): - * - ENABLED by default for third-party profiles (settings, cliproxy) - * - DISABLED for native Claude accounts (account, default profiles) - * - User can override via config: hooks.block_image_read.enabled: false - * - * Usage: - * Configured in ~/.claude/settings.json: - * { - * "hooks": { - * "PreToolUse": [{ - * "matcher": "Read", - * "hooks": [{ - * "type": "command", - * "command": "node ~/.ccs/hooks/block-image-read.cjs", - * "timeout": 5 - * }] - * }] - * } - * } - * - * Environment Variables (set by CCS): - * CCS_BLOCK_IMAGE_READ=1 - Enable blocking (default for third-party) - * CCS_BLOCK_IMAGE_READ=0 - Disable blocking - * CCS_PROFILE_TYPE - Profile type (account, default, settings, cliproxy) - * CCS_DEBUG=1 - Enable debug output - * - * Exit codes: - * 0 - Allow tool (pass-through) - * 2 - Block tool (deny with message) - * - * @module hooks/block-image-read - */ - -// Image file extensions to block -const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|webp|gif|bmp|tiff|tif|ico|svg|heic|heif|avif)$/i; - -// Read input from stdin -let input = ''; -process.stdin.setEncoding('utf8'); -process.stdin.on('data', (chunk) => { - input += chunk; -}); -process.stdin.on('end', () => { - processHook(); -}); - -// Handle stdin not being available -process.stdin.on('error', () => { - process.exit(0); -}); - -/** - * Check if hook should skip (for native Claude accounts). - * Matches WebSearch hook pattern. - */ -function shouldSkipHook() { - // Account/default profiles use native Claude - don't block - const profileType = process.env.CCS_PROFILE_TYPE; - if (profileType === 'account' || profileType === 'default') { - if (process.env.CCS_DEBUG) { - console.error(`[CCS Hook] Skipping image block for profile type: ${profileType}`); - } - return true; - } - - // Explicit disable via config - if (process.env.CCS_BLOCK_IMAGE_READ === '0') { - if (process.env.CCS_DEBUG) { - console.error('[CCS Hook] Image read blocking disabled by config'); - } - return true; - } - - return false; -} - -/** - * Main hook processing logic - */ -function processHook() { - try { - // Skip for native accounts or explicit disable - if (shouldSkipHook()) { - process.exit(0); - } - - const data = JSON.parse(input); - - // Only handle Read tool - if (data.tool_name !== 'Read') { - process.exit(0); - } - - const filePath = data.tool_input?.file_path || ''; - - if (process.env.CCS_DEBUG) { - console.error(`[CCS Hook] Read intercepted: ${filePath}`); - } - - // Check if file is an image - if (IMAGE_EXTENSIONS.test(filePath)) { - if (process.env.CCS_DEBUG) { - console.error(`[CCS Hook] Blocking image read: ${filePath}`); - } - outputBlock(filePath); - return; - } - - // Allow non-image files - process.exit(0); - } catch (err) { - if (process.env.CCS_DEBUG) { - console.error('[CCS Hook] Parse error:', err.message); - } - // Don't block on parse errors - process.exit(0); - } -} - -/** - * Output block response and exit - */ -function outputBlock(filePath) { - // Extract just the filename for cleaner display - const fileName = filePath.split(/[/\\]/).pop() || filePath; - - const message = [ - '[Image Read Blocked - Context Protection]', - '', - `File: ${fileName}`, - `Path: ${filePath}`, - '', - 'Image files consume 100K+ tokens each and will exhaust context.', - '', - 'The image was generated successfully. To view it:', - ' - Open the file path above in your image viewer', - ' - Use your file manager to navigate to the location', - ' - On macOS: open "' + filePath + '"', - ' - On Linux: xdg-open "' + filePath + '"', - ' - On Windows: start "" "' + filePath + '"', - '', - 'If you need to analyze the image, use the ai-multimodal skill', - 'which processes images via Gemini API without loading into context.', - ].join('\n'); - - const output = { - decision: 'block', - reason: 'Image file blocked to prevent context overflow', - // User-facing message (shows in CLI output) - systemMessage: `[Image Read Blocked] ${fileName} - Open file directly to view.`, - hookSpecificOutput: { - hookEventName: 'PreToolUse', - permissionDecision: 'deny', - // Claude reads this - explains what happened and alternatives - permissionDecisionReason: message, - }, - }; - - console.log(JSON.stringify(output)); - process.exit(2); -} diff --git a/lib/hooks/image-analyzer-transformer.cjs b/lib/hooks/image-analyzer-transformer.cjs index 1369b690..fd1504ec 100755 --- a/lib/hooks/image-analyzer-transformer.cjs +++ b/lib/hooks/image-analyzer-transformer.cjs @@ -196,6 +196,10 @@ function getModelsToTry() { */ async function analyzeWithRetry(base64Data, mediaType, timeoutMs) { const models = getModelsToTry(); + // Defensive check - should never happen but provides clear error + if (models.length === 0) { + throw new Error('No models configured for image analysis'); + } let lastError = null; for (let i = 0; i < models.length; i++) { @@ -636,6 +640,38 @@ function outputUnknownError(filePath, error) { process.exit(2); } +/** + * CLIProxy unavailable fallback - blocks Read to prevent context overflow + * When CLIProxy is not running, we cannot analyze the image. + * Blocking prevents the image from loading into Claude's context (100K+ tokens). + */ +function outputCliProxyUnavailableFallback(filePath) { + const fileName = filePath.split(/[/\\]/).pop() || filePath; + + // Keep message minimal to avoid context pollution and hallucination + const message = [ + '[Image Read Blocked]', + '', + `File: ${fileName}`, + '', + 'CLIProxy unavailable. Image blocked to prevent context overflow.', + ].join('\n'); + + const output = { + decision: 'block', + reason: 'CLIProxy unavailable - image blocked to prevent context overflow', + systemMessage: `[Image Blocked] ${fileName} - CLIProxy unavailable. Start: ccs config`, + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: message, + }, + }; + + console.log(JSON.stringify(output)); + process.exit(2); +} + /** * Output success response and exit */ @@ -763,12 +799,12 @@ async function processHook() { // Check CLIProxy availability const cliProxyAvailable = await isCliProxyAvailable(); if (!cliProxyAvailable) { - debugLog('Skipping: CLIProxy not available', { + debugLog('Blocking: CLIProxy not available', { endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`, - action: 'passing through to native Read', + action: 'blocking to prevent context overflow', }); - // Pass through to native Read - process.exit(0); + outputCliProxyUnavailableFallback(filePath); + return; } const model = getModelForProvider(); diff --git a/src/ccs.ts b/src/ccs.ts index 7c04006d..1d447e48 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -13,7 +13,6 @@ import { ensureProfileHooks, } from './utils/websearch-manager'; import { getGlobalEnvConfig } from './config/unified-config-loader'; -import { getImageReadBlockHookEnv } from './utils/hooks/image-read-block-hook-env'; import { ensureProfileHooks as ensureImageAnalyzerHooks } from './utils/hooks/image-analyzer-profile-hook-injector'; import { fail, info } from './utils/ui'; @@ -166,12 +165,10 @@ async function execClaudeWithProxy( const isWindows = process.platform === 'win32'; const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); const webSearchEnv = getWebSearchHookEnv(); - const imageReadBlockEnv = getImageReadBlockHookEnv(); const env = { ...process.env, ...envVars, ...webSearchEnv, - ...imageReadBlockEnv, CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider }; @@ -622,7 +619,6 @@ async function main(): Promise { // Use --settings flag (backward compatible) const expandedSettingsPath = getSettingsPath(profileInfo.name); const webSearchEnv = getWebSearchHookEnv(); - const imageReadBlockEnv = getImageReadBlockHookEnv(); // Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles const globalEnvConfig = getGlobalEnvConfig(); const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {}; @@ -644,7 +640,6 @@ async function main(): Promise { ...globalEnv, ...settingsEnv, // Explicitly inject all settings env vars ...webSearchEnv, - ...imageReadBlockEnv, CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider }; execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars); diff --git a/src/cliproxy/cliproxy-executor.ts b/src/cliproxy/cliproxy-executor.ts index 808a1814..0d640a8e 100644 --- a/src/cliproxy/cliproxy-executor.ts +++ b/src/cliproxy/cliproxy-executor.ts @@ -37,7 +37,6 @@ import { DEFAULT_BACKEND } from './platform-detector'; import { configureProviderModel, getCurrentModel } from './model-config'; import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver'; import { getWebSearchHookEnv } from '../utils/websearch-manager'; -import { getImageReadBlockHookEnv } from '../utils/hooks/image-read-block-hook-env'; import { getImageAnalysisHookEnv } from '../utils/hooks/get-image-analysis-hook-env'; import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from './model-catalog'; import { CodexReasoningProxy } from './codex-reasoning-proxy'; @@ -969,13 +968,11 @@ export async function execClaudeWithCLIProxy( ANTHROPIC_BASE_URL: finalBaseUrl, }; const webSearchEnv = getWebSearchHookEnv(); - const imageReadBlockEnv = getImageReadBlockHookEnv(); const imageAnalysisEnv = getImageAnalysisHookEnv(provider); const env = { ...process.env, ...effectiveEnvVars, ...webSearchEnv, - ...imageReadBlockEnv, ...imageAnalysisEnv, CCS_PROFILE_TYPE: 'cliproxy', // Signal to WebSearch hook this is a third-party provider }; diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 15a65998..5a8dcffe 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -545,6 +545,7 @@ export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = { kiro: 'kiro-claude-haiku-4-5', ghcp: 'claude-haiku-4.5', claude: 'claude-haiku-4-5-20251001', + // 'vision-model' is a generic placeholder - users can override via config.yaml qwen: 'vision-model', iflow: 'qwen3-vl-plus', }, diff --git a/src/utils/hooks/image-analyzer-profile-hook-injector.ts b/src/utils/hooks/image-analyzer-profile-hook-injector.ts index cd7a4db4..94d414da 100644 --- a/src/utils/hooks/image-analyzer-profile-hook-injector.ts +++ b/src/utils/hooks/image-analyzer-profile-hook-injector.ts @@ -4,7 +4,7 @@ * Injects image analyzer hooks into per-profile settings files. * This replaces the global ~/.claude/settings.json approach. * - * Only injects for CLIProxy profiles (agy, gemini) that support vision analysis. + * Injects for profiles configured in image_analysis.provider_models. * * @module utils/hooks/image-analyzer-profile-injector */ diff --git a/src/utils/hooks/image-read-block-hook-env.ts b/src/utils/hooks/image-read-block-hook-env.ts deleted file mode 100644 index b2cc91fc..00000000 --- a/src/utils/hooks/image-read-block-hook-env.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Image Read Block Hook Environment Variables - * - * Provides environment variables for image read blocking hook configuration. - * Prevents context overflow when skills generate images and agent tries to read them. - * - * Enabled by default for third-party profiles (settings, cliproxy). - * Disabled for native Claude accounts where context is managed server-side. - * - * @module utils/hooks/image-read-block-hook-env - */ - -import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader'; - -/** - * Configuration for image read blocking. - */ -export interface ImageReadBlockConfig { - /** Whether blocking is enabled (default: true) */ - enabled: boolean; -} - -/** - * Get image read block configuration from unified config. - * Defaults to ENABLED (opt-out feature) - matches WebSearch pattern. - */ -export function getImageReadBlockConfig(): ImageReadBlockConfig { - const config = loadOrCreateUnifiedConfig(); - // Access hooks config via type assertion since it's a new field not yet in UnifiedConfig type - const hooksConfig = ( - config as unknown as { hooks?: { block_image_read?: { enabled?: boolean } } } - ).hooks; - return { - // Default to TRUE - enabled by default, user can opt-out - enabled: hooksConfig?.block_image_read?.enabled ?? true, - }; -} - -/** - * Get environment variables for image read block hook configuration. - * - * Like WebSearch, this respects CCS_PROFILE_TYPE: - * - 'account' or 'default' profiles: Skip blocking (native Claude) - * - 'settings' or 'cliproxy' profiles: Apply blocking - * - * @returns Record of environment variables to set before spawning Claude - */ -export function getImageReadBlockHookEnv(): Record { - const config = getImageReadBlockConfig(); - const env: Record = {}; - - if (config.enabled) { - env.CCS_BLOCK_IMAGE_READ = '1'; - } else { - // Explicit disable signal - env.CCS_BLOCK_IMAGE_READ = '0'; - } - - return env; -} diff --git a/src/utils/hooks/index.ts b/src/utils/hooks/index.ts index d952a096..7e05c9d5 100644 --- a/src/utils/hooks/index.ts +++ b/src/utils/hooks/index.ts @@ -6,7 +6,6 @@ * @module utils/hooks */ -export { getImageReadBlockHookEnv, getImageReadBlockConfig } from './image-read-block-hook-env'; export { getImageAnalysisHookEnv } from './get-image-analysis-hook-env'; export { getImageAnalyzerHookPath, diff --git a/src/utils/image-analysis/hook-installer.ts b/src/utils/image-analysis/hook-installer.ts index 99c4f624..3832131b 100644 --- a/src/utils/image-analysis/hook-installer.ts +++ b/src/utils/image-analysis/hook-installer.ts @@ -1,9 +1,7 @@ /** * Image Analysis Hook Installer * - * Manages installation of: - * 1. block-image-read.cjs hook (blocks image reads to prevent context overflow) - * 2. Prompt templates for image analysis (user-customizable) + * Manages installation of prompt templates for image analysis (user-customizable). * * @module utils/image-analysis/hook-installer */ @@ -13,16 +11,6 @@ import * as path from 'path'; import { info, warn } from '../ui'; import { getCcsDir } from '../config-manager'; -// Hook file name -const IMAGE_BLOCK_HOOK = 'block-image-read.cjs'; - -/** - * Get path to installed hook script - */ -export function getHookPath(): string { - return path.join(getCcsDir(), 'hooks', IMAGE_BLOCK_HOOK); -} - /** * Get CCS hooks directory */ @@ -37,72 +25,6 @@ export function getPromptsDir(): string { return path.join(getCcsDir(), 'prompts', 'image-analysis'); } -/** - * Check if image block hook is installed - */ -export function hasImageBlockHook(): boolean { - return fs.existsSync(getHookPath()); -} - -/** - * Install image block hook to ~/.ccs/hooks/ - * - * This hook intercepts Read tool calls for image files and blocks them - * to prevent context overflow (images consume 100K+ tokens each). - * - * @returns true if hook installed successfully - */ -export function installImageBlockHook(): boolean { - try { - // Ensure hooks directory exists - const hooksDir = getCcsHooksDir(); - if (!fs.existsSync(hooksDir)) { - fs.mkdirSync(hooksDir, { recursive: true, mode: 0o700 }); - } - - const hookPath = getHookPath(); - - // Find the bundled hook script - // In npm package: node_modules/ccs/lib/hooks/ - // In development: lib/hooks/ - const possiblePaths = [ - path.join(__dirname, '..', '..', '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK), - path.join(__dirname, '..', '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK), - path.join(__dirname, '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK), - ]; - - let sourcePath: string | null = null; - for (const p of possiblePaths) { - if (fs.existsSync(p)) { - sourcePath = p; - break; - } - } - - if (!sourcePath) { - if (process.env.CCS_DEBUG) { - console.error(warn(`Image block hook source not found: ${IMAGE_BLOCK_HOOK}`)); - } - return false; - } - - // Copy hook to ~/.ccs/hooks/ - fs.copyFileSync(sourcePath, hookPath); - fs.chmodSync(hookPath, 0o755); - - if (process.env.CCS_DEBUG) { - console.error(info(`Installed image block hook: ${hookPath}`)); - } - - return true; - } catch (error) { - if (process.env.CCS_DEBUG) { - console.error(warn(`Failed to install image block hook: ${(error as Error).message}`)); - } - return false; - } -} - /** * Install prompt templates to ~/.ccs/prompts/image-analysis/ * Only installs if directory doesn't exist (doesn't overwrite user edits) @@ -179,28 +101,3 @@ export function installImageAnalysisPrompts(): boolean { return false; } } - -/** - * Uninstall image block hook from ~/.ccs/hooks/ - * - * @returns true if hook uninstalled successfully - */ -export function uninstallImageBlockHook(): boolean { - try { - const hookPath = getHookPath(); - - if (fs.existsSync(hookPath)) { - fs.unlinkSync(hookPath); - if (process.env.CCS_DEBUG) { - console.error(info(`Uninstalled image block hook: ${hookPath}`)); - } - } - - return true; - } catch (error) { - if (process.env.CCS_DEBUG) { - console.error(warn(`Failed to uninstall image block hook: ${(error as Error).message}`)); - } - return false; - } -} diff --git a/src/utils/image-analysis/index.ts b/src/utils/image-analysis/index.ts index 1fea2b08..3a322ac9 100644 --- a/src/utils/image-analysis/index.ts +++ b/src/utils/image-analysis/index.ts @@ -1,15 +1,7 @@ /** * Image Analysis Utilities * - * Exports hook installer functions for image blocking and prompt management + * Exports hook installer functions for prompt management */ -export { - getHookPath, - getCcsHooksDir, - getPromptsDir, - hasImageBlockHook, - installImageBlockHook, - installImageAnalysisPrompts, - uninstallImageBlockHook, -} from './hook-installer'; +export { getCcsHooksDir, getPromptsDir, installImageAnalysisPrompts } from './hook-installer'; diff --git a/src/utils/shell-executor.ts b/src/utils/shell-executor.ts index d60e3574..9ee00511 100644 --- a/src/utils/shell-executor.ts +++ b/src/utils/shell-executor.ts @@ -7,7 +7,6 @@ import { spawn, ChildProcess } from 'child_process'; import { ErrorManager } from './error-manager'; import { getWebSearchHookEnv } from './websearch-manager'; -import { getImageReadBlockHookEnv } from './hooks/image-read-block-hook-env'; /** * Escape arguments for shell execution (Windows compatibility) @@ -53,12 +52,11 @@ export function execClaude( // Get WebSearch hook config env vars const webSearchEnv = getWebSearchHookEnv(); - const imageReadBlockEnv = getImageReadBlockHookEnv(); // Prepare environment (merge with process.env if envVars provided) const env = envVars - ? { ...process.env, ...envVars, ...webSearchEnv, ...imageReadBlockEnv } - : { ...process.env, ...webSearchEnv, ...imageReadBlockEnv }; + ? { ...process.env, ...envVars, ...webSearchEnv } + : { ...process.env, ...webSearchEnv }; let child: ChildProcess; if (needsShell) {