HookStackGitHub
Back to catalogue
ValidationStopStopWhen the agent finishes its task· non-blocking

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.

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": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/stop-registry-drift-check.mjs"
          }
        ]
      }
    ]
  }
}

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

#!/usr/bin/env node
// 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 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from '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);
}