HookStack
Back to catalogue
ValidationStop

Registry drift check at stop

Catalogue drift caught before CI rejects the PR

Safety net for the registry sync: runs sync-hooks --check when the session stops and blocks with a resync instruction if registry.json has drifted from the .mjs files on disk (e.g. the auto-sync hook failed silently mid-session). No-op on projects without sync-hooks.mjs.

What does the Registry drift check at stop hook do?

Registry drift check at stop is a Claude Code Stop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Catalogue drift caught before CI rejects the PR.

As a Stop hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Catch a silently failed auto-sync before pushing
  • Guarantee code_snippet always matches the dogfooded scripts at session end

Tags

#validation#sync#registry#safety-net

settings.json fragment

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/stop-registry-drift-check.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/stop-registry-drift-check.mjs

#!/usr/bin/env node
// @hookstack stop-registry-drift-check
// Filet de sécurité : vérifie au Stop que registry.json est synchrone avec les
// .mjs sur disque (sync-hooks --check). Attrape une sync auto qui aurait échoué
// silencieusement en cours de session, avant que la CI ne rejette la PR. (Stop)
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

function makeDefaultExec(projectDir) {
	return () =>
		execSync("node .claude/sync-hooks.mjs --check", {
			timeout: 30_000,
			cwd: projectDir,
			encoding: "utf8",
			stdio: "pipe",
		});
}

export function run({
	exec,
	exists = existsSync,
	projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {}) {
	// Projet sans sync-hooks (hook installé ailleurs que sur Hookstack) → no-op
	if (!exists(join(projectDir, ".claude/sync-hooks.mjs"))) return null;

	const doExec = exec ?? makeDefaultExec(projectDir);
	try {
		doExec();
		return null; // synchrone → silencieux
	} catch (e) {
		const out = `${e.stdout ?? ""}${e.stderr ?? ""}`
			.toString()
			.trim()
			.slice(-1000);
		return {
			exitCode: 2,
			message:
				`[registry-drift-check] registry.json a dérivé des .mjs sur disque :\n${out}\n` +
				`→ Lancer 'node .claude/sync-hooks.mjs' pour resynchroniser avant de terminer.\n`,
		};
	}
}

/* v8 ignore next 6 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const result = run();
	if (result?.message) process.stderr.write(result.message);
	if (result?.exitCode) process.exit(result.exitCode);
}

Learn more

Related hooks