claudepoint
Version:
The safest way to experiment with Claude Code. Create instant checkpoints, experiment fearlessly, restore instantly.
310 lines (262 loc) • 10.1 kB
JavaScript
/**
* ClaudePoint Hook Helper
* Integrates with Claude Code hooks for automatic checkpoint creation
*
* This script is called by Claude Code hooks and handles:
* - Safety checkpoints before bulk operations
* - Optional changelog integration
* - Smart batching to avoid checkpoint spam
*/
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { promises as fsPromises } from 'fs';
import CheckpointManager from '../src/lib/checkpoint-manager.js';
import { program } from 'commander';
import { createRequire } from 'module';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
program
.name('claudepoint-hook')
.description('ClaudePoint hook integration for Claude Code')
.version(packageJson.version);
program
.option('--trigger <type>', 'Hook trigger type')
.option('--tool <name>', 'Tool name that triggered the hook')
.option('--debug', 'Enable debug output')
.parse();
const options = program.opts();
async function logHookAttempt(projectDir, message, isError = false) {
try {
const logFile = join(projectDir, '.claudepoint', 'hooks.log');
const timestamp = new Date().toISOString();
const level = isError ? 'ERROR' : 'INFO';
const logEntry = `[${timestamp}] ${level}: ${message}\n`;
await fsPromises.appendFile(logFile, logEntry);
} catch (error) {
// Silently fail if logging fails - don't break the hook
}
}
async function findProjectDirectory() {
// Strategy 1: Use env var if set
if (process.env.CLAUDEPOINT_PROJECT_DIR) {
return process.env.CLAUDEPOINT_PROJECT_DIR;
}
// Strategy 2: Look for .claudepoint directory starting from cwd
let currentDir = process.cwd();
const root = '/';
while (currentDir !== root) {
const checkpointDir = join(currentDir, '.claudepoint');
try {
await fsPromises.access(checkpointDir);
return currentDir; // Found .claudepoint directory
} catch (error) {
// Continue searching
}
currentDir = dirname(currentDir);
}
// Strategy 3: Fallback to cwd
return process.cwd();
}
async function main() {
try {
// Get project directory with smart detection
const projectDir = await findProjectDirectory();
const manager = new CheckpointManager(projectDir);
await logHookAttempt(projectDir, `Hook triggered: ${options.trigger} for tool ${options.tool}`);
if (options.debug) {
console.error(`[claudepoint-hook] Project dir: ${projectDir}`);
console.error(`[claudepoint-hook] Trigger: ${options.trigger}`);
console.error(`[claudepoint-hook] Tool: ${options.tool}`);
}
// Check if ClaudePoint is set up in this project
const checkpointsDir = join(projectDir, '.claudepoint');
try {
await fsPromises.access(checkpointsDir);
} catch (error) {
await logHookAttempt(projectDir, 'ClaudePoint not set up in this project, skipping hook');
if (options.debug) {
console.error('[claudepoint-hook] ClaudePoint not set up in this project, exiting');
}
return; // Silently exit if ClaudePoint isn't set up
}
// Load hooks configuration
const hooksConfig = await manager.loadHooksConfig();
if (!hooksConfig.enabled) {
await logHookAttempt(projectDir, 'Hooks are disabled in this project, skipping');
if (options.debug) {
console.error('[claudepoint-hook] Hooks disabled, exiting');
}
return;
}
// Handle different trigger types
switch (options.trigger) {
case 'before_bulk_edit':
await handleBeforeBulkEdit(manager, hooksConfig, options, projectDir);
break;
case 'before_major_write':
await handleBeforeMajorWrite(manager, hooksConfig, options, projectDir);
break;
case 'before_bash_commands':
await handleBeforeBashCommands(manager, hooksConfig, options, projectDir);
break;
case 'before_file_operations':
await handleBeforeFileOperations(manager, hooksConfig, options, projectDir);
break;
default:
if (options.debug) {
console.error(`[claudepoint-hook] Unknown trigger: ${options.trigger}`);
}
}
} catch (error) {
try {
const projectDir = await findProjectDirectory();
await logHookAttempt(projectDir, `Hook failed: ${error.message}`, true);
} catch (logError) {
// Ignore logging errors
}
if (options.debug) {
console.error('[claudepoint-hook] Error:', error.message);
console.error('[claudepoint-hook] Stack:', error.stack);
}
process.exit(1);
}
}
async function handleBeforeBulkEdit(manager, config, options, projectDir) {
const trigger = config.triggers?.before_bulk_edit;
if (!trigger?.enabled) {
await logHookAttempt(projectDir, `Trigger before_bulk_edit is disabled, skipping`);
if (options.debug) {
console.error('[claudepoint-hook] before_bulk_edit trigger disabled');
}
return;
}
// Create safety checkpoint
const description = `Safety checkpoint before ${options.tool || 'bulk edit'}`;
try {
await logHookAttempt(projectDir, `Creating safety checkpoint: ${description}`);
await manager.ensureDirectories();
const result = await manager.create(null, description, false); // Don't force, respect anti-spam
if (result.success) {
await logHookAttempt(projectDir, `Successfully created checkpoint: ${result.name}`);
if (options.debug) {
console.error(`[claudepoint-hook] Created safety checkpoint: ${result.name}`);
}
// Add changelog entry if enabled
if (config.auto_changelog) {
await manager.logToChangelog(
'SAFETY_CHECKPOINT',
`Created safety checkpoint before ${options.tool || 'bulk operation'}`,
`Automatic checkpoint created by ClaudePoint hooks`
);
}
} else if (result.noChanges) {
if (options.debug) {
console.error(`[claudepoint-hook] No changes detected, skipping checkpoint`);
}
// This is not an error - just no changes to checkpoint
} else if (result.tooRecent) {
await logHookAttempt(projectDir, `Skipping checkpoint - too recent: ${result.error}`);
if (options.debug) {
console.error(`[claudepoint-hook] ${result.error}`);
}
// This is not an error - just anti-spam protection
} else {
if (options.debug) {
console.error(`[claudepoint-hook] Failed to create checkpoint: ${result.error}`);
}
}
} catch (error) {
if (options.debug) {
console.error('[claudepoint-hook] Error creating checkpoint:', error.message);
}
}
}
async function handleBeforeMajorWrite(manager, config, options, projectDir) {
const trigger = config.triggers?.before_major_write;
if (!trigger?.enabled) {
return;
}
// Similar logic to bulk edit but for major file writes
await handleBeforeBulkEdit(manager, config, options);
}
async function handleBeforeBashCommands(manager, config, options, projectDir) {
const trigger = config.triggers?.before_bash_commands;
if (!trigger?.enabled) {
if (options.debug) {
console.error('[claudepoint-hook] before_bash_commands trigger disabled');
}
return;
}
// Create safety checkpoint before bash commands
const description = `Safety checkpoint before bash command execution`;
try {
await manager.ensureDirectories();
const result = await manager.create(null, description, false); // Don't force, respect anti-spam
if (result.success) {
if (options.debug) {
console.error(`[claudepoint-hook] Created safety checkpoint: ${result.name}`);
}
// Add changelog entry if enabled
if (config.auto_changelog) {
await manager.logToChangelog(
'SAFETY_CHECKPOINT',
`Created safety checkpoint before bash command execution`,
`Automatic checkpoint created by ClaudePoint hooks before executing bash commands`
);
}
} else if (result.noChanges) {
if (options.debug) {
console.error(`[claudepoint-hook] No changes detected, skipping checkpoint`);
}
}
} catch (error) {
if (options.debug) {
console.error('[claudepoint-hook] Error creating checkpoint:', error.message);
}
}
}
async function handleBeforeFileOperations(manager, config, options, projectDir) {
const trigger = config.triggers?.before_file_operations;
if (!trigger?.enabled) {
if (options.debug) {
console.error('[claudepoint-hook] before_file_operations trigger disabled');
}
return;
}
// Create safety checkpoint before any file operations (comprehensive protection)
const description = `Safety checkpoint before ${options.tool || 'file'} operation`;
try {
await manager.ensureDirectories();
const result = await manager.create(null, description, false); // Don't force, respect anti-spam
if (result.success) {
if (options.debug) {
console.error(`[claudepoint-hook] Created safety checkpoint: ${result.name}`);
}
// Add changelog entry if enabled
if (config.auto_changelog) {
await manager.logToChangelog(
'SAFETY_CHECKPOINT',
`Created safety checkpoint before ${options.tool || 'file'} operation`,
`Automatic checkpoint created by ClaudePoint hooks for comprehensive file operation protection`
);
}
} else if (result.noChanges) {
if (options.debug) {
console.error(`[claudepoint-hook] No changes detected, skipping checkpoint`);
}
}
} catch (error) {
if (options.debug) {
console.error('[claudepoint-hook] Error creating checkpoint:', error.message);
}
}
}
main().catch(error => {
if (options.debug) {
console.error('Unhandled error in claudepoint-hook:', error);
}
process.exit(1);
});