mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 00:22:34 +00:00
Match WebSearch hook pattern: - ENABLED by default for settings/cliproxy profiles - DISABLED for native Claude accounts (account/default) - User can override via config: hooks.block_image_read.enabled: false This ensures CCS CLI users get context protection out-of-the-box while native Claude subscription users are unaffected.
171 lines
4.7 KiB
JavaScript
171 lines
4.7 KiB
JavaScript
#!/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);
|
|
}
|