mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
refactor(hooks): deprecate block-image-read, add CLIProxy fallback
- Remove redundant block-image-read.cjs hook (image-analyzer handles all) - Add fallback blocking when CLIProxy unavailable (prevents context overflow) - Simplify fallback message to prevent context pollution/hallucination - Remove image-read-block-hook-env.ts and related exports - Update comments per PR #442 review suggestions - Add defensive check for empty models array Closes #426
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
...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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<string, string> {
|
||||
const config = getImageReadBlockConfig();
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
if (config.enabled) {
|
||||
env.CCS_BLOCK_IMAGE_READ = '1';
|
||||
} else {
|
||||
// Explicit disable signal
|
||||
env.CCS_BLOCK_IMAGE_READ = '0';
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user