#!/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: decision === 'allow' }; if (systemMessage) { response.systemMessage = systemMessage; } console.log(JSON.stringify(response)); } /** * 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'; if (tool_name === 'Edit' || tool_name === 'Write') { const result = await handleFileModification(tool_input, sessionId); decision = result.decision; reason = result.reason; } else if (tool_name === 'Bash') { const result = await handleBashCommand(tool_input, sessionId); decision = result.decision; reason = result.reason; } // Wait for async audit logging to complete before disconnecting await new Promise(resolve => setTimeout(resolve, 500)); await mongoose.disconnect(); outputResponse(decision, reason); 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 || ''; // 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 }; const boundaryResult = BoundaryEnforcer.enforce(action, context); // If boundary enforcer blocks, deny the action if (!boundaryResult.allowed) { return { decision: 'deny', reason: boundaryResult.message || 'Boundary violation detected' }; } // 2. HARD BLOCK: instruction-history.json modifications (inst_027) if (filePath.includes('instruction-history.json')) { 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) { const validateAction = { type: 'modify_governance_file', description: `Modifying ${path.basename(filePath)}`, file: filePath }; CrossReferenceValidator.validate(validateAction, { ...context, governance: true }); } // 4. Metacognitive verification for security-critical files const securityFiles = ['auth', 'security', 'credential', 'jwt', 'password', 'secret']; const isSecurityFile = securityFiles.some(keyword => filePath.toLowerCase().includes(keyword)); if (isSecurityFile) { const verifyAction = { type: 'modify_security_file', description: `Modifying security-critical file: ${filePath}`, file: filePath }; const reasoning = `Modifying security-critical file: ${filePath}. Automated approval for documentation/comments.`; MetacognitiveVerifier.verify(verifyAction, reasoning, { ...context, security_impact: true, automated_approval: true }); } // 5. Context pressure monitoring (on every tool use) const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service'); ContextPressureMonitor.analyzePressure({ sessionId, tool: 'Edit/Write', action: 'file_modification', file: filePath }); // 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) { 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) { const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service'); 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)) }); } return { decision: 'allow', reason: `Framework audit complete: ${path.basename(filePath)}` }; } /** * Handle Bash command execution */ async function handleBashCommand(toolInput, sessionId) { const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service'); const command = toolInput.command || ''; // 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); if (!result.allowed) { return { decision: 'deny', reason: result.message || 'Bash command blocked by BoundaryEnforcer' }; } // 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) }); return { decision: 'allow', reason: 'Bash command allowed' }; } // Run hook main().catch(err => { outputResponse('allow', `Fatal error: ${err.message}`); process.exit(0); });