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>
351 lines
10 KiB
JavaScript
Executable file
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);
|
|
});
|