mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 06:17:27 +00:00
feat(hooks): add block-image-read hook to prevent context overflow
Add PreToolUse hook that intercepts Read tool calls on image files (.png, .jpg, .webp, etc.) and blocks them with helpful message. This prevents context exhaustion when image generation skills produce multiple files and the agent tries to read them (each image can consume 100K+ tokens). Configuration: - Enable via config.yaml: hooks.block_image_read.enabled: true - Or env var: CCS_BLOCK_IMAGE_READ=1 Hook integration: - lib/hooks/block-image-read.cjs - the hook script - src/utils/hooks/image-read-block-hook-env.ts - config loader - Integrated into all spawn locations (ccs.ts, shell-executor, cliproxy-executor) Closes #426
This commit is contained in:
1 parent
24b03121fd
commit
38eb74043c
6 files changed
+221
-2
No files matched your search
@@ -0,0 +1,150 @@
|
||||
#!/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.
|
||||
*
|
||||
* 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:
|
||||
* CCS_BLOCK_IMAGE_READ=1 - Enable blocking (required)
|
||||
* CCS_BLOCK_IMAGE_READ=0 - Disable blocking (allow all reads)
|
||||
* 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 blocking is enabled via environment variable
|
||||
*/
|
||||
function isBlockingEnabled() {
|
||||
// Must be explicitly enabled
|
||||
return process.env.CCS_BLOCK_IMAGE_READ === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main hook processing logic
|
||||
*/
|
||||
function processHook() {
|
||||
try {
|
||||
// Skip if blocking not enabled
|
||||
if (!isBlockingEnabled()) {
|
||||
if (process.env.CCS_DEBUG) {
|
||||
console.error('[CCS Hook] Image read blocking disabled (CCS_BLOCK_IMAGE_READ != 1)');
|
||||
}
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user