tractatus/.claude/hooks/framework-audit-hook.js
TheFlow 436ca56cb0 feat(governance): implement comprehensive enforcement architecture
Completes enforcement implementation from ENFORCEMENT_AUDIT.md analysis:

 Implemented (6 enforcement mechanisms):
1. Token checkpoint monitoring (inst_075)
   - .claude/hooks/check-token-checkpoint.js
   - PostToolUse hook integration

2. Trigger word detection (inst_078, inst_082)
   - .claude/hooks/trigger-word-checker.js (already completed)
   - "ff" and "ffs" triggers architecturally enforced

3. Framework activity verification (inst_064)
   - Enhanced scripts/session-init.js with fade detection
   - Alerts when components stale >20 messages

4. Test requirement enforcement (inst_068)
   - Enhanced .git/hooks/pre-commit
   - Runs tests if test files exist for modified code
   - Blocks commits on test failures

5. Background process tracking (inst_023)
   - scripts/track-background-process.js
   - Integrated into session-init.js and session-closedown.js
   - Tracks persistent vs temporary processes

6. Security logging verification (inst_046)
   - scripts/verify-security-logging.js
   - Can be integrated into deployment workflow

7. Meta-enforcement monitoring system
   - scripts/audit-enforcement.js
   - Scans HIGH persistence instructions for imperatives
   - Reports enforcement gaps (currently 28/39 gaps)

🔒 Protection Added:
- inst_027: Hard block on instruction-history.json edits
- Conventional commit format enforcement (inst_066)
- CSP + test validation in pre-commit hook

📊 Current Enforcement Status:
- Baseline: 11/39 imperative instructions enforced (28%)
- Framework fade detection operational
- Token checkpoints architecturally monitored

🎯 Philosophy:
"If it's MANDATORY, it must be ENFORCED architecturally, not documented."

This addresses the root cause of voluntary compliance failures identified
when Claude missed "ffs" trigger and token checkpoints despite active
HIGH persistence instructions.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 13:15:06 +13:00

351 lines
10 KiB
JavaScript
Executable file

#!/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);
});