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
+72 -1
View File
@@ -15,6 +15,8 @@
* codegraph query <search> Search for symbols
* codegraph files [options] Show project file structure
* codegraph context <task> Build context for a task
* codegraph mark-dirty [path] Mark project as needing sync (hooks)
* codegraph sync-if-dirty [path] Sync if marked dirty (hooks)
*
* Note: Git hooks have been removed. CodeGraph sync is triggered automatically
* through codegraph's Claude Code hooks integration.
@@ -23,7 +25,7 @@
import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
import CodeGraph from '../index';
import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index';
import type { IndexProgress } from '../index';
import { runInstaller } from '../installer';
import { initSentry, captureException } from '../sentry';
@@ -926,6 +928,75 @@ program
}
});
/**
* codegraph mark-dirty [path]
*
* Touches .codegraph/.dirty to signal that files have changed.
* Used by Claude Code PostToolUse hooks to batch syncs.
* Runs silently and always exits 0.
*/
program
.command('mark-dirty [path]')
.description('Mark project as needing sync (used by Claude Code hooks)')
.action(async (pathArg: string | undefined) => {
try {
const startPath = path.resolve(pathArg || process.cwd());
const projectRoot = findNearestCodeGraphRoot(startPath);
if (!projectRoot) {
// No .codegraph/ found — exit silently
process.exit(0);
}
const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
fs.writeFileSync(dirtyPath, Date.now().toString(), 'utf-8');
} catch {
// Never fail — this runs in the background during edits
}
process.exit(0);
});
/**
* codegraph sync-if-dirty [path]
*
* Syncs the index only if .codegraph/.dirty exists.
* Removes the marker BEFORE syncing so edits during sync
* create a new marker for the next Stop event.
* Runs silently and always exits 0.
*/
program
.command('sync-if-dirty [path]')
.description('Sync if project was marked dirty (used by Claude Code hooks)')
.action(async (pathArg: string | undefined) => {
try {
const startPath = path.resolve(pathArg || process.cwd());
const projectRoot = findNearestCodeGraphRoot(startPath);
if (!projectRoot) {
process.exit(0);
}
const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
// No marker → nothing to do (sub-ms exit)
if (!fs.existsSync(dirtyPath)) {
process.exit(0);
}
// Remove marker FIRST so edits during sync create a new one
try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ }
// If not fully initialized (no DB), exit
if (!CodeGraph.isInitialized(projectRoot)) {
process.exit(0);
}
// Run sync
const cg = await CodeGraph.open(projectRoot);
await cg.sync();
cg.destroy();
} catch {
// Never fail — this runs at the end of Claude responses
}
process.exit(0);
});
/**
* codegraph install
*/