Back to catalogue
ValidationStopStopWhen the agent finishes its task· non-blocking
Automatic quality check (Stop)
Runs a quality check on every stop: type-check (tsc), ESLint (only if an ESLint config file is present), and tests. Blocks the agent from handing back until all checks pass.
Use cases
- Automatic Definition of Done with no manual intervention
- Block sessions that leave lint errors or insufficient coverage
- Parallelize build + tests to minimize wait time
Providers & tags
Claude Code
#quality#tests#coverage#lint#definition-of-done#asyncRewake
settings.json fragment
{
"hooks": {
"Stop": [
{
"hooks": [
{
"asyncRewake": true,
"command": "node .claude/hooks/quality-check.mjs",
"rewakeMessage": "Des erreurs qualité ont été détectées après la dernière modification :",
"rewakeSummary": "Erreurs qualité détectées",
"statusMessage": "Vérification qualité en cours...",
"timeout": 600,
"type": "command"
}
]
}
]
}
}Script · .claude/hooks/quality-check.mjs
#!/usr/bin/env node
// Bilan qualité complet à la fin d'une session : typecheck + lint + tests (Stop)
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
export function run({
exec,
exists = existsSync,
projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {}) {
const doExec =
exec ?? ((cmd) => execSync(cmd, { cwd: projectDir, stdio: 'pipe', timeout: 60_000 }));
const messages = [];
function check(label, cmd) {
try {
doExec(cmd);
messages.push(`[quality-check] ✓ ${label}\n`);
return true;
} catch (err) {
const out = err.stdout?.toString()?.trim() ?? '';
messages.push(`[quality-check] ✗ ${label}\n${out ? out.slice(-500) + '\n' : ''}`);
return false;
}
}
const checks = [];
const hasPkg = exists(join(projectDir, 'package.json'));
if (hasPkg && exists(join(projectDir, 'tsconfig.json')))
checks.push(['TypeScript', 'npx --no-install tsc --noEmit']);
const eslintConfigs = ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs', '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yml', '.eslintrc.yaml', '.eslintrc'];
if (hasPkg && eslintConfigs.some((f) => exists(join(projectDir, f))))
checks.push(['ESLint', 'npx --no-install eslint --max-warnings=0 .']);
if (hasPkg)
checks.push(['Tests', 'pnpm test --run 2>/dev/null || yarn test --run 2>/dev/null || bun test 2>/dev/null || npm test --if-present 2>/dev/null || npx --no-install vitest run 2>/dev/null']);
const results = checks.map(([label, cmd]) => check(label, cmd));
const failed = results.filter((r) => !r).length;
if (failed > 0) messages.push(`[quality-check] ${failed}/${checks.length} vérification(s) échouée(s).\n`);
else if (checks.length > 0) messages.push('[quality-check] ✓ Tous les contrôles qualité passent.\n');
return { checks: checks.length, failed, message: messages.join('') };
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const result = run();
process.stderr.write(result.message);
if (result.failed > 0) process.exit(2);
}