HookStackGitHub
Back to catalogue
DocumentationFileChanged· README.md

Docs consistency reminder

No more READMEs telling two different stories

When a README changes, injects a reminder listing the sibling READMEs (repo root + packages/*) that carry the same product promise — CLI examples, slugs and wording must stay consistent across them. Pure context injection, never blocks.

What does the Docs consistency reminder hook do?

Docs consistency reminder is a Claude Code FileChanged hook matching README.md. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. No more READMEs telling two different stories.

Use cases

  • Propagate a CLI flag rename from the npm README to the GitHub README
  • Keep the website pitch and both READMEs telling the same story

Tags

#documentation#readme#consistency#marketing

settings.json fragment

{
  "hooks": {
    "FileChanged": [
      {
        "matcher": "README.md",
        "hooks": [
          {
            "type": "command",
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/file-changed-docs-consistency.mjs"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/file-changed-docs-consistency.mjs

#!/usr/bin/env node
// Rappelle de propager les changements d'un README vers les surfaces sœurs
// (FileChanged README.md). Quand un README change, liste les autres README du
// repo (racine + packages/*) qui portent la même promesse produit et doivent
// rester cohérents (exemples CLI, slugs, wording).
import { readFileSync, existsSync, readdirSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';

// Trouve les README "surfaces produit" : racine + packages/*/README.md.
export function findSiblingReadmes({
  exists = existsSync,
  readdir = readdirSync,
  projectDir,
} = {}) {
  const surfaces = [];
  if (exists(join(projectDir, 'README.md'))) surfaces.push('README.md');
  const pkgsDir = join(projectDir, 'packages');
  if (exists(pkgsDir)) {
    try {
      for (const pkg of readdir(pkgsDir)) {
        if (exists(join(pkgsDir, pkg, 'README.md'))) surfaces.push(`packages/${pkg}/README.md`);
      }
    } catch {}
  }
  return surfaces;
}

export function run(input, {
  exists = existsSync,
  readdir = readdirSync,
  projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {}) {
  const filePath = input.file_path ?? '';
  if (!filePath.endsWith('README.md') || input.event === 'unlink') return null;

  const changed = filePath.startsWith(`${projectDir}/`)
    ? filePath.slice(projectDir.length + 1)
    : filePath;
  const siblings = findSiblingReadmes({ exists, readdir, projectDir }).filter((s) => s !== changed);
  if (!siblings.length) return null;

  return {
    hookSpecificOutput: {
      hookEventName: 'FileChanged',
      additionalContext:
        `${changed} changed. These sibling docs share the same product promise and must stay consistent ` +
        `(CLI examples, slugs, wording): ${siblings.join(', ')}. ` +
        'Check whether the change needs to be mirrored there (and on the website copy if user-facing).',
    },
  };
}

/* 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));
}