Force implementation doc at stop
No code change ships without a doc/implementation/ trace
Blocks the session from ending if source files were modified but no doc/implementation/ file was created or updated. Detects changes via git diff and untracked files; ignores tests, config, and internal .claude/ scripts. No-op on non-git projects.
What does the Force implementation doc at stop hook do?
Force implementation doc 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. No code change ships without a doc/implementation/ trace.
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
- Zero documentation debt after an agent session
- Onboarding — always find a doc/implementation/<feature>.md explaining the why and how
- Maintenance — never guess what the agent did from git diff alone
Tags
settings.json fragment
{
"hooks": {
"Stop": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/stop-force-implementation-doc.mjs",
"type": "command"
}
]
}
]
}
}Script · .claude/hooks/stop-force-implementation-doc.mjs
#!/usr/bin/env node
// @hookstack stop-force-implementation-doc
// Bloque la fin de session si du code source a été modifié sans qu'un fichier
// okf/implementation/ ait été créé ou mis à jour. Garantit zéro dette de doc. (Stop)
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
function defaultExec(cmd, opts) {
return execSync(cmd, { encoding: "utf8", timeout: 10_000, ...opts });
}
const CODE_EXTS =
/\.(ts|tsx|mjs|cjs|js|jsx|css|scss|sass|py|go|rs|java|rb|php|swift|kt)$/;
const SKIP_PATH =
/^(node_modules\/|\.git\/|dist\/|build\/|out\/|\.next\/|tests?\/|__tests?__\/|doc\/|docs\/|\.claude\/|public\/)/;
const SKIP_NAME = /(\.(test|spec)\.[a-z]+$|\.json$|\.md$|\.lock$|\.txt$|\.env)/;
function isSourceFile(p) {
return CODE_EXTS.test(p) && !SKIP_PATH.test(p) && !SKIP_NAME.test(p);
}
function isImplDoc(p) {
return p.startsWith("okf/implementation/");
}
export function run(
_input,
{
exec = defaultExec,
projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {},
) {
let files;
try {
const modified = exec("git diff --name-only HEAD", { cwd: projectDir });
const untracked = exec("git ls-files --others --exclude-standard", {
cwd: projectDir,
});
files = [
...new Set(
[...modified.split("\n"), ...untracked.split("\n")]
.map((l) => l.trim())
.filter(Boolean),
),
];
} catch {
return null; // pas un dépôt git ou git absent → no-op
}
if (!files.some(isSourceFile)) return null;
if (files.some(isImplDoc)) return null;
return {
exitCode: 2,
message:
`[force-implementation-doc] Source code was modified but okf/implementation/ was not updated.\n` +
`→ Create or update okf/implementation/<feature-name>.md explaining your technical choices\n` +
` and the implementation before ending this session.\n`,
};
}
/* v8 ignore next 6 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, "utf8"));
const result = run(input);
if (result?.message) process.stderr.write(result.message);
if (result?.exitCode) process.exit(result.exitCode);
}
Learn more
Related hooks
- Session summary generationAn automatic changelog of what the agent shipped
- Save compaction summary to logKeep a readable trail of every compaction
- Docs consistency reminderNo more READMEs telling two different stories
- OKF bundle staleness reminder on session startNever let your project knowledge base go stale and lie to you