CC-301c · Module 2
Stop Hook → Type Check → Fix → Commit Loop
4 min read
The canonical quality loop is the single most valuable automation pattern in Claude Code. It transforms every session into a self-correcting pipeline: Claude writes code, the stop hook checks it, errors feed back to Claude, Claude fixes them, the hook checks again, and when everything passes, the code auto-commits. No human intervention required at any step.
The loop has four stages. Stage 1: Claude finishes a task and the stop hook fires. Stage 2: the hook runs quality checks — TypeScript type checking (tsc --noEmit), linting (eslint), and optionally tests (vitest run). Stage 3: if checks fail, the hook returns a blocking instruction with the error details. Claude receives the errors, analyzes them, and generates fixes. Stage 4: Claude finishes the fixes, the stop hook fires again, and the cycle repeats from Stage 2. When all checks pass, the hook returns an instruction to commit the changes.
The elegance of this loop is that it leverages Claude's strongest capability — fixing errors when given explicit error messages — in a fully automated pipeline. Claude is remarkably good at fixing TypeScript errors when you show it the exact error message and file location. The stop hook provides exactly that: "src/components/Button.tsx:24:5 - error TS2322: Type string is not assignable to type number." Claude reads the error, opens the file, fixes the type mismatch, and saves. The hook fires again, tsc passes, and the commit happens.
The critical design constraint is a maximum iteration count. Without it, a genuinely unfixable error (a circular dependency, a missing package, a fundamental architecture mismatch) creates an infinite loop: check → fail → attempt fix → check → fail → attempt fix. Set a maximum of three iterations. If the same error persists after three fix attempts, the hook escalates to the user instead of retrying. Three attempts is enough for genuine fixes. More than three means the problem requires human judgment.
// Simplified quality loop stop hook
const MAX_RETRIES = 3;
let retryCount = parseInt(process.env.HOOK_RETRY_COUNT || '0');
// 1. Check for file changes
const diff = execSync('git diff --name-only', { encoding: 'utf-8' });
if (!diff.trim()) process.exit(0); // Nothing changed
// 2. Run type check (capture output, don't let it hit stdout)
const tscResult = execSync('npx tsc --noEmit 2>&1', {
encoding: 'utf-8',
env: { ...process.env, npm_config_loglevel: 'silent' },
}).catch(e => e.stdout);
// 3. If errors, feed back to Claude (with retry guard)
if (tscResult?.includes('error TS')) {
if (retryCount >= MAX_RETRIES) {
console.log(JSON.stringify({
decision: 'block',
reason: `TypeScript errors persist after ${MAX_RETRIES} attempts. Please review manually.`
}));
} else {
console.log(JSON.stringify({
decision: 'block',
reason: `Fix these TypeScript errors:\n${tscResult}`
}));
}
process.exit(0);
}
// 4. All checks pass — auto-commit
console.log(JSON.stringify({
decision: 'block',
reason: 'All checks pass. Commit these changes with a descriptive message.'
}));