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": [
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/stop-force-implementation-doc.mjs"
}
]
}
]
}
}Script · .claude/hooks/stop-force-implementation-doc.mjs
#!/usr/bin/env node
// Bloque la fin de session si du code source a été modifié sans qu'un fichier
// doc/implementation/ ait été créé ou mis à jour. Garantit zéro dette de doc. (Stop)
import { execSync } from 'child_process';
import { readFileSync } from 'fs';
import { fileURLToPath } from '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('doc/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 doc/implementation/ was not updated.\n` +
`→ Create or update doc/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
- Dead link checkerNo broken links in Markdown docs you ship — checked automatically at session end