#!/usr/bin/env node /** * Tractatus Framework Audit Hook (PreToolUse) * * Automatically invokes framework services during Claude Code tool execution * and logs all decisions to the audit database for dashboard visibility. * * Hook Input (JSON via stdin): * { * "session_id": "abc123", * "hook_event_name": "PreToolUse", * "tool_name": "Write", * "tool_input": { "file_path": "/path", "content": "..." } * } * * Hook Output (JSON to stdout): * { * "hookSpecificOutput": { * "hookEventName": "PreToolUse", * "permissionDecision": "allow|deny|ask", * "permissionDecisionReason": "explanation" * }, * "continue": true, * "suppressOutput": false * } * * Exit Codes: * - 0: Success (allow tool execution) * - 2: Block tool execution */ const path = require('path'); const fs = require('fs'); /** * Read JSON input from stdin */ function readStdin() { return new Promise((resolve, reject) => { let data = ''; process.stdin.on('data', chunk => { data += chunk; }); process.stdin.on('end', () => { try { resolve(JSON.parse(data)); } catch (err) { reject(new Error('Invalid JSON input')); } }); process.stdin.on('error', reject); }); } /** * Output hook response */ function outputResponse(decision, reason, systemMessage = null) { const response = { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: decision, permissionDecisionReason: reason }, continue: true, suppressOutput: false // FIXED: Always show framework guidance to Claude }; if (systemMessage) { response.systemMessage = systemMessage; } console.log(JSON.stringify(response)); } /** * Log denial to audit database */ async function logDenial(toolName, toolInput, sessionId, violations, reason, metadata = {}) { try { const AuditLog = require('../../src/models/AuditLog.model'); const auditEntry = new AuditLog({ sessionId: sessionId || 'unknown', action: 'hook_denial', allowed: false, rulesChecked: violations.map(v => v.ruleId).filter(Boolean), violations: violations, metadata: { ...metadata, tool: toolName, file_path: toolInput.file_path || toolInput.path, command: toolInput.command, denial_reason: reason, hook: 'framework-audit-hook' }, domain: 'SYSTEM', service: 'PreToolUseHook', environment: 'development', timestamp: new Date() }); await auditEntry.save(); } catch (err) { // Non-fatal: Continue with denial even if logging fails console.error('[Framework Audit] Failed to log denial:', err.message); } } /** * Main hook logic */ async function main() { let input; try { input = await readStdin(); } catch (err) { // Invalid input, allow execution outputResponse('allow', 'Invalid hook input'); process.exit(0); } const { session_id, tool_name, tool_input } = input; // Skip framework for non-invasive tools if (['Read', 'Glob', 'Grep'].includes(tool_name)) { outputResponse('allow', 'Read-only tool, no framework check needed'); process.exit(0); } // Connect to MongoDB and invoke framework const mongoose = require('mongoose'); try { await mongoose.connect('mongodb://localhost:27017/tractatus_dev', { serverSelectionTimeoutMS: 2000 }); } catch (err) { // MongoDB not available, allow execution but log warning outputResponse('allow', 'Framework unavailable (MongoDB not connected)'); process.exit(0); } // Import all 6 framework services const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); const CrossReferenceValidator = require('../../src/services/CrossReferenceValidator.service'); const MetacognitiveVerifier = require('../../src/services/MetacognitiveVerifier.service'); const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service'); const InstructionPersistenceClassifier = require('../../src/services/InstructionPersistenceClassifier.service'); const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service'); // CRITICAL: Initialize all services so audit logging works await BoundaryEnforcer.initialize(); await CrossReferenceValidator.initialize(); await MetacognitiveVerifier.initialize(); await ContextPressureMonitor.initialize(session_id); await InstructionPersistenceClassifier.initialize(); await PluralisticDeliberationOrchestrator.initialize(); const sessionId = session_id || 'claude-code-session'; try { // Route to framework service based on tool let decision = 'allow'; let reason = 'Framework check passed'; let systemMessage = null; // PHASE 3: Collect systemMessage if (tool_name === 'Edit' || tool_name === 'Write') { const result = await handleFileModification(tool_input, sessionId); decision = result.decision; reason = result.reason; systemMessage = result.systemMessage; // PHASE 3: Capture systemMessage } else if (tool_name === 'Bash') { const result = await handleBashCommand(tool_input, sessionId); decision = result.decision; reason = result.reason; systemMessage = result.systemMessage; // PHASE 3: Capture systemMessage } // Wait for async audit logging to complete before disconnecting await new Promise(resolve => setTimeout(resolve, 500)); await mongoose.disconnect(); outputResponse(decision, reason, systemMessage); // PHASE 3: Pass systemMessage process.exit(decision === 'deny' ? 2 : 0); } catch (err) { await mongoose.disconnect(); // Framework error - allow execution but log outputResponse('allow', `Framework error: ${err.message}`); process.exit(0); } } /** * Handle file modifications (Edit, Write tools) */ async function handleFileModification(toolInput, sessionId) { const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); const CrossReferenceValidator = require('../../src/services/CrossReferenceValidator.service'); const MetacognitiveVerifier = require('../../src/services/MetacognitiveVerifier.service'); const filePath = toolInput.file_path || toolInput.path || 'unknown'; const content = toolInput.new_string || toolInput.content || ''; // PHASE 3.5: Cross-validate prompt analysis vs action analysis let promptAnalysis = null; let crossValidation = { hasPromptAnalysis: false, agreements: [], disagreements: [], missedFlags: [], validationScore: 0 }; try { const sessionStatePath = path.join(__dirname, '..', 'session-state.json'); if (fs.existsSync(sessionStatePath)) { const sessionState = JSON.parse(fs.readFileSync(sessionStatePath, 'utf-8')); promptAnalysis = sessionState.promptAnalysis?.latest || null; if (promptAnalysis) { crossValidation.hasPromptAnalysis = true; // Detect action characteristics const lowerPath = filePath.toLowerCase(); const lowerContent = content.toLowerCase(); const actionFlags = { isSchemaChange: lowerPath.includes('model') || lowerPath.includes('schema') || lowerContent.includes('schema') || lowerContent.includes('mongoose'), isSecurityChange: lowerPath.includes('auth') || lowerPath.includes('jwt') || lowerPath.includes('security') || lowerContent.includes('password'), isServiceFile: lowerPath.includes('service.js'), isHookFile: lowerPath.includes('hook'), isConfigFile: lowerPath.includes('config') }; // Compare prompt expectations vs actual action // 1. Schema change validation if (promptAnalysis.flags.schemaChange === true && actionFlags.isSchemaChange === true) { crossValidation.agreements.push('schema_change_detected'); } else if (promptAnalysis.flags.schemaChange === true && actionFlags.isSchemaChange === false) { crossValidation.disagreements.push('prompt_expected_schema_but_action_is_not'); } else if (promptAnalysis.flags.schemaChange === false && actionFlags.isSchemaChange === true) { crossValidation.missedFlags.push('action_is_schema_but_prompt_didnt_detect'); } // 2. Security change validation if (promptAnalysis.flags.securityChange === true && actionFlags.isSecurityChange === true) { crossValidation.agreements.push('security_change_detected'); } else if (promptAnalysis.flags.securityChange === true && actionFlags.isSecurityChange === false) { crossValidation.disagreements.push('prompt_expected_security_but_action_is_not'); } else if (promptAnalysis.flags.securityChange === false && actionFlags.isSecurityChange === true) { crossValidation.missedFlags.push('action_is_security_but_prompt_didnt_detect'); } // Calculate validation score (0-100) const totalChecks = crossValidation.agreements.length + crossValidation.disagreements.length + crossValidation.missedFlags.length; if (totalChecks > 0) { crossValidation.validationScore = Math.round( (crossValidation.agreements.length / totalChecks) * 100 ); } else { // No flags to validate - perfect alignment crossValidation.validationScore = 100; } } } } catch (err) { // Non-fatal: Continue without cross-validation console.error('[Framework Audit] Cross-validation error:', err.message); } // PHASE 3: Collect guidance from all framework services const guidanceMessages = []; // Track service coordination for Deep Interlock logging const servicesInvolved = []; // 1. Boundary enforcement const action = { type: 'file_modification', description: `Modify ${path.basename(filePath)}`, target: filePath, content_length: content.length }; const context = { sessionId, tool: 'Edit/Write', file: filePath, services_involved: servicesInvolved // Pass array reference for coordination tracking }; servicesInvolved.push('BoundaryEnforcer'); // Track service involvement BEFORE calling const boundaryResult = BoundaryEnforcer.enforce(action, context); // PHASE 3: Collect guidance from BoundaryEnforcer if (boundaryResult.guidance && boundaryResult.guidance.systemMessage) { guidanceMessages.push(boundaryResult.guidance.systemMessage); } // If boundary enforcer blocks, determine if human approval needed or hard deny if (!boundaryResult.allowed) { const violations = boundaryResult.violations || [{ ruleId: boundaryResult.ruleId || 'boundary_violation', ruleText: boundaryResult.message || 'Boundary violation detected', severity: 'HIGH', details: boundaryResult.message }]; // Check if human judgment is required (Tractatus boundaries) const requiresHumanJudgment = boundaryResult.humanRequired || boundaryResult.human_required; if (requiresHumanJudgment) { // Escalate to human via Claude Code's permission system return { decision: 'ask', reason: boundaryResult.message || 'This decision requires human judgment', systemMessage: guidanceMessages.length > 0 ? guidanceMessages.join('\n') : undefined }; } // Hard block - no human override possible await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, boundaryResult.message, { boundary: boundaryResult.boundary, domain: boundaryResult.domain }); return { decision: 'deny', reason: boundaryResult.message || 'Boundary violation detected', systemMessage: guidanceMessages.length > 0 ? guidanceMessages.join('\n') : undefined }; } // 1a. PHASE 2: Semantic detection - schema changes const schemaDetection = BoundaryEnforcer.detectSchemaChange(filePath, content); if (schemaDetection.isSchemaChange) { // Schema change detected - invoke CrossReferenceValidator const schemaValidation = await CrossReferenceValidator.validateSchemaChange({ type: 'schema_modification', description: `${schemaDetection.isSensitiveCollection ? 'SENSITIVE ' : ''}Schema change in ${path.basename(filePath)}`, file: filePath, severity: schemaDetection.severity, patterns: schemaDetection.detectedPatterns }, { ...context, schema_change: true, sensitive_collection: schemaDetection.isSensitiveCollection }); // PHASE 3: Collect guidance from schema validation if (schemaValidation.guidance && schemaValidation.guidance.systemMessage) { guidanceMessages.push(schemaValidation.guidance.systemMessage); } // If critical conflicts found, block the action if (!schemaValidation.allowed && schemaValidation.criticalConflicts > 0) { const violations = schemaValidation.violations || [{ ruleId: 'schema_conflict', ruleText: schemaValidation.recommendation, severity: 'CRITICAL', details: `Critical schema conflicts: ${schemaValidation.criticalConflicts}` }]; await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, schemaValidation.recommendation, { schema_change: true, sensitive_collection: schemaDetection.isSensitiveCollection, critical_conflicts: schemaValidation.criticalConflicts }); return { decision: 'deny', reason: `BLOCKED: ${schemaValidation.recommendation}`, systemMessage: guidanceMessages.join('\n') // Include collected guidance }; } } // 1b. PHASE 2: Semantic detection - security gradient const securityGradient = BoundaryEnforcer.detectSecurityGradient(filePath, content); // 2. HARD BLOCK: instruction-history.json modifications (inst_027) if (filePath.includes('instruction-history.json')) { const violations = [{ ruleId: 'inst_027', ruleText: 'NEVER modify instruction-history.json without explicit human approval', severity: 'CRITICAL', details: 'Manual edits risk corrupting the governance system. Use scripts/add-instruction.js instead.' }]; await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, 'inst_027 violation', { governance_file: true }); return { decision: 'deny', reason: 'BLOCKED by inst_027: NEVER modify instruction-history.json without explicit human approval. Use scripts/add-instruction.js or similar tools instead. Manual edits risk corrupting the governance system.' }; } // 3. Validate against instructions for governance files const governanceFiles = [ 'CLAUDE.md', 'auth.middleware.js', 'auth.controller.js', 'session-state.json' ]; const isGovernanceFile = governanceFiles.some(f => filePath.includes(f)); if (isGovernanceFile) { servicesInvolved.push('CrossReferenceValidator'); // Track BEFORE calling const validateAction = { type: 'modify_governance_file', description: `Modifying ${path.basename(filePath)}`, file: filePath }; CrossReferenceValidator.validate(validateAction, { ...context, governance: true }); } // 4. PHASE 2: Graduated security verification based on content analysis if (securityGradient !== 'ROUTINE') { const verifyAction = { type: 'modify_security_file', description: `[${securityGradient}] Modifying ${path.basename(filePath)}`, file: filePath, security_gradient: securityGradient }; // Determine automated_approval based on security gradient const canAutoApprove = securityGradient === 'DOCUMENTATION'; const reasoning = canAutoApprove ? `${securityGradient} level change to ${filePath}. Documentation/comment update - automated approval.` : `${securityGradient} level security modification detected in ${filePath}. Requires elevated scrutiny.`; servicesInvolved.push('MetacognitiveVerifier'); // Track BEFORE calling const verificationResult = MetacognitiveVerifier.verify(verifyAction, reasoning, { ...context, security_impact: securityGradient === 'CRITICAL' || securityGradient === 'HIGH', security_gradient: securityGradient, automated_approval: canAutoApprove, requires_human_review: securityGradient === 'CRITICAL' }); // PHASE 3: Collect guidance from MetacognitiveVerifier if (verificationResult && verificationResult.guidance && verificationResult.guidance.systemMessage) { guidanceMessages.push(verificationResult.guidance.systemMessage); } } // 5. Context pressure monitoring (on every tool use) servicesInvolved.push('ContextPressureMonitor'); // Track BEFORE calling const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service'); ContextPressureMonitor.analyzePressure({ sessionId, tool: 'Edit/Write', action: 'file_modification', file: filePath, services_involved: servicesInvolved // Pass coordination tracking }); // 6. Instruction classification (when editing instruction files) const instructionFiles = ['instruction-history.json', 'CLAUDE.md', 'settings.json']; const isInstructionFile = instructionFiles.some(f => filePath.includes(f)); if (isInstructionFile) { servicesInvolved.push('InstructionPersistenceClassifier'); // Track BEFORE calling const InstructionPersistenceClassifier = require('../../src/services/InstructionPersistenceClassifier.service'); InstructionPersistenceClassifier.classify({ text: `File modification: ${path.basename(filePath)}`, context: { ...context, file: filePath, contentLength: content.length }, timestamp: new Date(), source: 'tool_use' }); } // 7. Pluralistic deliberation (when value conflicts might occur) const valueConflictFiles = ['auth', 'security', 'privacy', 'accessibility', 'performance']; const hasValueConflict = valueConflictFiles.some(keyword => filePath.toLowerCase().includes(keyword)); if (hasValueConflict) { servicesInvolved.push('PluralisticDeliberationOrchestrator'); // Track BEFORE calling const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service'); const deliberationResult = PluralisticDeliberationOrchestrator.analyzeConflict({ type: 'file_modification', description: `Modifying file with potential value conflicts: ${path.basename(filePath)}`, file: filePath }, { ...context, value_domains: valueConflictFiles.filter(k => filePath.toLowerCase().includes(k)) }); // PHASE 3: Collect guidance from PluralisticDeliberationOrchestrator if (deliberationResult && deliberationResult.guidance && deliberationResult.guidance.systemMessage) { guidanceMessages.push(deliberationResult.guidance.systemMessage); } } // PHASE 3.5: Add cross-validation feedback to systemMessage if (crossValidation.hasPromptAnalysis) { if (crossValidation.missedFlags.length > 0) { guidanceMessages.push( `\n⚠️ CROSS-VALIDATION: Prompt analysis missed ${crossValidation.missedFlags.length} flag(s):\n` + crossValidation.missedFlags.map(f => ` • ${f}`).join('\n') ); } if (crossValidation.disagreements.length > 0) { guidanceMessages.push( `\n⚠️ CROSS-VALIDATION: ${crossValidation.disagreements.length} disagreement(s) between prompt and action:\n` + crossValidation.disagreements.map(d => ` • ${d}`).join('\n') ); } if (crossValidation.validationScore < 80 && crossValidation.validationScore > 0) { guidanceMessages.push( `\n📊 VALIDATION SCORE: ${crossValidation.validationScore}% (prompt-action alignment)` ); } } // PHASE 3: Aggregate guidance and return with systemMessage const result = { decision: 'allow', reason: `Framework audit complete: ${path.basename(filePath)}`, crossValidation, // PHASE 3.5: Include validation data servicesInvolved // Include Deep Interlock coordination data }; // Add systemMessage if we have any guidance if (guidanceMessages.length > 0) { result.systemMessage = guidanceMessages.join('\n'); } return result; } /** * Handle Bash command execution */ async function handleBashCommand(toolInput, sessionId) { const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); const command = toolInput.command || ''; // PHASE 3: Collect guidance const guidanceMessages = []; // Check for cross-project commands const crossProjectPatterns = [ '/family-history/', '/sydigital/', 'cd ../family-history', 'cd ../sydigital' ]; const isCrossProject = crossProjectPatterns.some(pattern => command.includes(pattern)); const action = { type: 'bash_command', description: isCrossProject ? 'Cross-project bash command' : 'Bash command execution', target: command.substring(0, 100), cross_project: isCrossProject }; const context = { sessionId, tool: 'Bash', command: command.substring(0, 200) }; const result = BoundaryEnforcer.enforce(action, context); // PHASE 3: Collect guidance from BoundaryEnforcer if (result.guidance && result.guidance.systemMessage) { guidanceMessages.push(result.guidance.systemMessage); } if (!result.allowed) { const violations = result.violations || [{ ruleId: result.ruleId || 'bash_blocked', ruleText: result.message || 'Bash command blocked by BoundaryEnforcer', severity: 'HIGH', details: result.message }]; // Check if human judgment is required (Tractatus boundaries) const requiresHumanJudgment = result.humanRequired || result.human_required; if (requiresHumanJudgment) { // Escalate to human via Claude Code's permission system return { decision: 'ask', reason: result.message || 'This bash command requires human judgment', systemMessage: guidanceMessages.length > 0 ? guidanceMessages.join('\n') : undefined }; } // Hard block - no human override possible await logDenial('Bash', { command }, sessionId, violations, result.message, { cross_project: isCrossProject, boundary: result.boundary, domain: result.domain }); return { decision: 'deny', reason: result.message || 'Bash command blocked by BoundaryEnforcer', systemMessage: guidanceMessages.length > 0 ? guidanceMessages.join('\n') : undefined }; } // Context pressure monitoring for Bash commands const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service'); ContextPressureMonitor.analyzePressure({ sessionId, tool: 'Bash', action: 'bash_command', command: command.substring(0, 100) }); // PHASE 3: Return with aggregated guidance const response = { decision: 'allow', reason: 'Bash command allowed' }; if (guidanceMessages.length > 0) { response.systemMessage = guidanceMessages.join('\n'); } return response; } // Run hook main().catch(err => { outputResponse('allow', `Fatal error: ${err.message}`); process.exit(0); });