termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
326 lines (325 loc) • 13 kB
JavaScript
import { HookSchema } from "./types.js";
import { promises as fs } from "node:fs";
import path from "node:path";
export class HookValidator {
/**
* Validate a hook configuration
*/
async validate(hook) {
const errors = [];
const warnings = [];
try {
// Schema validation
HookSchema.parse(hook);
}
catch (error) {
if (error.errors) {
errors.push(...error.errors.map((e) => `${e.path.join('.')}: ${e.message}`));
}
else {
errors.push(`Schema validation failed: ${error.message}`);
}
}
// Handler validation
const handlerValidation = await this.validateHandler(hook);
errors.push(...handlerValidation.errors);
warnings.push(...handlerValidation.warnings);
// Security validation
const securityValidation = this.validateSecurity(hook);
errors.push(...securityValidation.errors);
warnings.push(...securityValidation.warnings);
// Performance validation
const performanceValidation = this.validatePerformance(hook);
warnings.push(...performanceValidation.warnings);
// Logic validation
const logicValidation = this.validateLogic(hook);
errors.push(...logicValidation.errors);
warnings.push(...logicValidation.warnings);
return {
valid: errors.length === 0,
errors,
warnings
};
}
/**
* Validate hook handler
*/
async validateHandler(hook) {
const errors = [];
const warnings = [];
switch (hook.handler.type) {
case 'builtin':
if (!hook.handler.builtin) {
errors.push('Builtin hook must specify builtin type');
}
break;
case 'javascript':
if (!hook.handler.script && !hook.handler.file) {
errors.push('JavaScript hook must provide script or file');
}
if (hook.handler.file) {
const fileValidation = await this.validateFile(hook.handler.file, 'javascript');
errors.push(...fileValidation.errors);
warnings.push(...fileValidation.warnings);
}
if (hook.handler.script) {
const scriptValidation = this.validateJavaScript(hook.handler.script);
errors.push(...scriptValidation.errors);
warnings.push(...scriptValidation.warnings);
}
break;
case 'python':
if (!hook.handler.script && !hook.handler.file) {
errors.push('Python hook must provide script or file');
}
if (hook.handler.file) {
const fileValidation = await this.validateFile(hook.handler.file, 'python');
errors.push(...fileValidation.errors);
warnings.push(...fileValidation.warnings);
}
if (hook.handler.script) {
const scriptValidation = this.validatePython(hook.handler.script);
warnings.push(...scriptValidation.warnings);
}
break;
case 'shell':
if (!hook.handler.script && !hook.handler.file) {
errors.push('Shell hook must provide script or file');
}
if (hook.handler.file) {
const fileValidation = await this.validateFile(hook.handler.file, 'shell');
errors.push(...fileValidation.errors);
warnings.push(...fileValidation.warnings);
}
if (hook.handler.script) {
const scriptValidation = this.validateShell(hook.handler.script);
warnings.push(...scriptValidation.warnings);
}
break;
default:
errors.push(`Unknown handler type: ${hook.handler.type}`);
}
return { errors, warnings };
}
/**
* Validate file existence and permissions
*/
async validateFile(filePath, type) {
const errors = [];
const warnings = [];
try {
const stats = await fs.stat(filePath);
if (!stats.isFile()) {
errors.push(`Hook file is not a regular file: ${filePath}`);
return { errors, warnings };
}
// Check file extension matches type
const ext = path.extname(filePath);
const expectedExtensions = {
javascript: ['.js', '.mjs', '.ts'],
python: ['.py'],
shell: ['.sh', '.bash']
};
if (expectedExtensions[type] && !expectedExtensions[type].includes(ext)) {
warnings.push(`File extension ${ext} doesn't match hook type ${type}`);
}
// Check file size
if (stats.size > 1024 * 1024) { // 1MB
warnings.push(`Hook file is large (${Math.round(stats.size / 1024)}KB) - may impact performance`);
}
// Check file permissions
try {
await fs.access(filePath, fs.constants.R_OK);
}
catch {
errors.push(`Hook file is not readable: ${filePath}`);
}
}
catch (error) {
if (error.code === 'ENOENT') {
errors.push(`Hook file not found: ${filePath}`);
}
else {
errors.push(`Failed to access hook file ${filePath}: ${error.message}`);
}
}
return { errors, warnings };
}
/**
* Validate JavaScript code
*/
validateJavaScript(script) {
const errors = [];
const warnings = [];
// Basic syntax validation
try {
new Function(script);
}
catch (error) {
errors.push(`JavaScript syntax error: ${error.message}`);
}
// Security checks
const dangerousPatterns = [
{ pattern: /eval\s*\(/, message: 'Use of eval() is dangerous' },
{ pattern: /Function\s*\(/, message: 'Dynamic function creation can be dangerous' },
{ pattern: /require\s*\(\s*['"]child_process['"]/, message: 'Direct child_process usage not allowed' },
{ pattern: /require\s*\(\s*['"]fs['"]/, message: 'Direct fs usage - use provided fs methods' },
{ pattern: /process\.exit/, message: 'process.exit() should not be used in hooks' },
{ pattern: /global\./, message: 'Global variable modification not recommended' }
];
for (const { pattern, message } of dangerousPatterns) {
if (pattern.test(script)) {
warnings.push(message);
}
}
// Check for common issues
if (script.length < 10) {
warnings.push('Hook script is very short - may not be functional');
}
if (!script.includes('return')) {
warnings.push('Hook script should return a result');
}
return { errors, warnings };
}
/**
* Validate Python code
*/
validatePython(script) {
const warnings = [];
// Security checks
const dangerousPatterns = [
{ pattern: /import\s+os/, message: 'Direct os module usage should be careful' },
{ pattern: /import\s+subprocess/, message: 'subprocess usage should be restricted' },
{ pattern: /exec\s*\(/, message: 'exec() usage can be dangerous' },
{ pattern: /eval\s*\(/, message: 'eval() usage can be dangerous' },
{ pattern: /open\s*\(/, message: 'File operations should be restricted to repo path' }
];
for (const { pattern, message } of dangerousPatterns) {
if (pattern.test(script)) {
warnings.push(message);
}
}
if (script.length < 10) {
warnings.push('Hook script is very short - may not be functional');
}
return { warnings };
}
/**
* Validate shell script
*/
validateShell(script) {
const warnings = [];
// Security checks
const dangerousPatterns = [
{ pattern: /rm\s+-rf/, message: 'rm -rf usage can be dangerous' },
{ pattern: /sudo\s+/, message: 'sudo usage in hooks not recommended' },
{ pattern: /curl.*\|.*sh/, message: 'Piping downloads to shell is dangerous' },
{ pattern: /wget.*\|.*sh/, message: 'Piping downloads to shell is dangerous' },
{ pattern: /dd\s+/, message: 'dd command usage can be dangerous' },
{ pattern: /mkfs/, message: 'Filesystem operations not allowed' }
];
for (const { pattern, message } of dangerousPatterns) {
if (pattern.test(script)) {
warnings.push(message);
}
}
// Check for missing error handling
if (!script.includes('set -e') && !script.includes('trap')) {
warnings.push('Consider adding error handling (set -e or trap)');
}
return { warnings };
}
/**
* Validate security aspects
*/
validateSecurity(hook) {
const errors = [];
const warnings = [];
// Check timeout values
if (hook.timeout > 300000) { // 5 minutes
warnings.push('Hook timeout is very long - may block other operations');
}
if (hook.timeout < 1000) {
warnings.push('Hook timeout is very short - may cause premature failures');
}
// Check priority values
if (hook.priority < 0) {
warnings.push('Negative priority values may cause unexpected execution order');
}
// Check retry values
if (hook.retries > 5) {
warnings.push('High retry count may cause long delays on failures');
}
// Check conditions for safety
if (hook.conditions) {
for (const condition of hook.conditions) {
if (condition.type === 'custom' && condition.condition.includes('rm ')) {
warnings.push('Custom condition contains potentially dangerous command');
}
}
}
return { errors, warnings };
}
/**
* Validate performance aspects
*/
validatePerformance(hook) {
const warnings = [];
// Check for performance issues
if (hook.type === 'PreToolUse' && hook.timeout > 10000) {
warnings.push('PreToolUse hook with long timeout may slow down operations');
}
if (hook.type === 'PostToolUse' && hook.priority < 50) {
warnings.push('PostToolUse hook with high priority may delay results');
}
// Check handler performance implications
if (hook.handler.type === 'python' && hook.timeout < 5000) {
warnings.push('Python hooks may need more time due to interpreter startup');
}
return { warnings };
}
/**
* Validate logical consistency
*/
validateLogic(hook) {
const errors = [];
const warnings = [];
// Check matcher logic
const { matcher } = hook;
// Warn about overly broad matchers
if (!matcher.toolNames && !matcher.patterns && !matcher.conditions &&
!matcher.fileTypes && !matcher.providers && !matcher.models) {
warnings.push('Hook matcher is very broad - will execute for all operations');
}
// Check for conflicting conditions
if (matcher.providers && matcher.models) {
warnings.push('Both provider and model filters specified - ensure compatibility');
}
// Check hook type and matcher compatibility
if (hook.type === 'PreDiff' && matcher.toolNames &&
!matcher.toolNames.some(name => name.includes('diff') || name.includes('edit'))) {
warnings.push('PreDiff hook matcher may not match diff operations');
}
if (hook.type === 'PreCommit' && matcher.toolNames &&
!matcher.toolNames.some(name => name.includes('git') || name.includes('commit'))) {
warnings.push('PreCommit hook matcher may not match commit operations');
}
return { errors, warnings };
}
/**
* Validate hook ID uniqueness within a set of hooks
*/
validateUniqueIds(hooks) {
const errors = [];
const seen = new Set();
for (const hook of hooks) {
if (seen.has(hook.id)) {
errors.push(`Duplicate hook ID: ${hook.id}`);
}
else {
seen.add(hook.id);
}
}
return { errors };
}
}