Add Claude Code hooks for automatic CodeGraph sync

PostToolUse(Edit|Write) marks the project dirty via .codegraph/.dirty,
and Stop syncs only if dirty — batching all edits into one sync per
Claude response. The installer now writes these hooks to settings.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-02-10 01:12:43 -06:00
co-authored by Claude Opus 4.6
parent 48cc3a8b66
commit f46ba02b87
4 changed files with 175 additions and 4 deletions
+87
View File
@@ -175,6 +175,93 @@ export function hasPermissions(location: InstallLocation): boolean {
return permissions.some((p: string) => p.startsWith('mcp__codegraph__'));
}
// =============================================================================
// Hooks Configuration
// =============================================================================
/**
* Get the hooks configuration for Claude Code auto-sync.
*
* PostToolUse(Edit|Write) → mark-dirty (async, non-blocking)
* Stop → sync-if-dirty (sync, ensures fresh index before next user turn)
*/
function getHooksConfig(location: InstallLocation): Record<string, any> {
const command = location === 'global' ? 'codegraph' : 'npx @colbymchenry/codegraph';
return {
PostToolUse: [
{
matcher: 'Edit|Write',
hooks: [
{
type: 'command',
command: `${command} mark-dirty`,
async: true,
},
],
},
],
Stop: [
{
hooks: [
{
type: 'command',
command: `${command} sync-if-dirty`,
},
],
},
],
};
}
/**
* Check if Claude Code hooks already exist for CodeGraph
*/
export function hasHooks(location: InstallLocation): boolean {
const settingsPath = getSettingsJsonPath(location);
const settings = readJsonFile(settingsPath);
const hooks = settings.hooks;
if (!hooks) return false;
// Check if any hook command references codegraph
const json = JSON.stringify(hooks);
return json.includes('codegraph mark-dirty') || json.includes('codegraph sync-if-dirty');
}
/**
* Write Claude Code hooks to settings.json for auto-sync.
* Merges with existing hooks, deduplicating any previous codegraph entries.
*/
export function writeHooks(location: InstallLocation): void {
const settingsPath = getSettingsJsonPath(location);
const settings = readJsonFile(settingsPath);
if (!settings.hooks) {
settings.hooks = {};
}
const newHooks = getHooksConfig(location);
// For each hook event (PostToolUse, Stop), merge with existing entries
for (const [event, newEntries] of Object.entries(newHooks)) {
if (!Array.isArray(settings.hooks[event])) {
settings.hooks[event] = [];
}
// Remove any existing codegraph entries for this event
settings.hooks[event] = (settings.hooks[event] as any[]).filter((entry: any) => {
// Keep entries that don't reference codegraph
const entryJson = JSON.stringify(entry);
return !entryJson.includes('codegraph mark-dirty') && !entryJson.includes('codegraph sync-if-dirty');
});
// Add new codegraph entries
settings.hooks[event].push(...(newEntries as any[]));
}
writeJsonFile(settingsPath, settings);
}
/**
* Get the path to CLAUDE.md
* - Global: ~/.claude/CLAUDE.md
+13 -3
View File
@@ -8,7 +8,7 @@
import { execSync } from 'child_process';
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
import { writeMcpConfig, writePermissions, writeClaudeMd, hasMcpConfig, hasPermissions } from './config-writer';
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
import CodeGraph from '../index';
/**
@@ -76,7 +76,17 @@ export async function runInstaller(): Promise<void> {
}
}
// Step 5: Write CLAUDE.md instructions
// Step 5: Write auto-sync hooks
const alreadyHasHooks = hasHooks(location);
writeHooks(location);
if (alreadyHasHooks) {
success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
} else {
success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
}
// Step 6: Write CLAUDE.md instructions
const claudeMdResult = writeClaudeMd(location);
const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
@@ -88,7 +98,7 @@ export async function runInstaller(): Promise<void> {
success(`Added CodeGraph instructions to ${claudeMdPath}`);
}
// Step 6: For local install, initialize the project
// Step 7: For local install, initialize the project
if (location === 'local') {
await initializeLocalProject();
}