HookStack
Back to catalogue
DocumentationStop

Session summary generation

An automatic changelog of what the agent shipped

At session end, captures the diff of modified files and appends it to a timestamped change log, creating an automatic changelog of what the agent produced.

What does the Session summary generation hook do?

Session summary generation is a Claude Code Stop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. An automatic changelog of what the agent shipped.

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

  • Automatic changelog
  • Change traceability
  • Post-hoc review

Tags

#documentation#changelog#git#summary

settings.json fragment

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/session-changelog.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/session-changelog.mjs

#!/usr/bin/env node
// @hookstack stop-generate-changelog
// Génère une entrée de changelog depuis le diff git de la session (Stop)
import { execSync } from "node:child_process";
import { appendFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";

export function run({
	exec,
	append = appendFileSync,
	exists = existsSync,
	projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
	now = () => new Date().toISOString(),
} = {}) {
	const doExec =
		exec ??
		((cmd) => {
			try {
				return execSync(cmd, {
					encoding: "utf8",
					timeout: 10_000,
					cwd: projectDir,
				}).trim();
			} catch {
				return "";
			}
		});

	const branch = doExec("git branch --show-current");
	const diff = doExec(
		"git diff --stat HEAD~1 HEAD 2>/dev/null || git diff --stat HEAD",
	);
	const commits = doExec('git log -5 --pretty="- %s (%h)"');

	if (!diff && !commits) return null;

	const date = now().split("T")[0];
	const entry = [
		`\n## ${date} — Session sur \`${branch || "main"}\``,
		"",
		commits ? `### Commits\n${commits}` : "",
		diff ? `### Fichiers modifiés\n\`\`\`\n${diff}\n\`\`\`` : "",
	]
		.filter(Boolean)
		.join("\n");

	const changelogPath = join(projectDir, "CHANGELOG.md");
	if (!exists(changelogPath)) {
		return {
			written: false,
			message: "[session-changelog] CHANGELOG.md absent — entrée ignorée.\n",
		};
	}

	append(changelogPath, `${entry}\n`);
	return {
		written: true,
		entry,
		message: "[session-changelog] ✓ Entrée ajoutée dans CHANGELOG.md\n",
	};
}

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

Learn more

Related hooks