HookStack
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.

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

  • 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": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/file-changed-docs-consistency.mjs",
            "type": "command"
          }
        ],
        "matcher": "README.md"
      }
    ]
  }
}

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

#!/usr/bin/env node
// @hookstack file-changed-docs-consistency
// 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 { existsSync, readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node: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));
}

Learn more

Related hooks