HookStackGitHub
Back to catalogue
ValidationFileChanged· registry.json

Registry schema validation on change

Schema errors surface in-session, not in CI

Validates registry/registry.json against registry.schema.json the moment the file changes, feeding validation errors back into the session instead of waiting for the CI to reject the push. Silent when the registry is valid.

What does the Registry schema validation on change hook do?

Registry schema validation on change is a Claude Code FileChanged hook matching registry.json. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Schema errors surface in-session, not in CI.

Use cases

  • Catch a missing required field right after editing a catalogue entry
  • Reject unknown fields before they ever reach a commit

Tags

#validation#schema#registry#feedback

settings.json fragment

{
  "hooks": {
    "FileChanged": [
      {
        "matcher": "registry.json",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/registry-validate-on-change.mjs"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/registry-validate-on-change.mjs

#!/usr/bin/env node
// Valide registry.json contre son schéma dès qu'il change (FileChanged registry.json)
// Feedback dans la session au lieu d'attendre l'échec de la CI.
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';

function defaultExec(projectDir) {
  return execSync('node registry/validate-registry.mjs', {
    timeout: 30_000,
    cwd: projectDir,
    encoding: 'utf8',
    stdio: 'pipe',
  });
}

export function run(input, { exec = defaultExec, projectDir = process.env.CLAUDE_PROJECT_DIR } = {}) {
  const filePath = input.file_path ?? '';
  if (!filePath.endsWith('registry/registry.json') || !projectDir) return null;

  try {
    exec(projectDir);
    return null; // valide → silencieux
  } catch (e) {
    const out = `${e.stdout ?? ''}${e.stderr ?? ''}`.toString().trim().slice(0, 1500);
    return {
      hookSpecificOutput: {
        hookEventName: 'FileChanged',
        additionalContext: `registry.json is INVALID against registry.schema.json:\n${out}\nFix the entries above before continuing.`,
      },
    };
  }
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const input = JSON.parse(readFileSync(0, 'utf8'));
  const result = run(input);
  if (result) process.stdout.write(JSON.stringify(result));
}